Skip to content

stream: Readable.concat util method - #40075

Open
tugrul wants to merge 1 commit into
nodejs:mainfrom
tugrul:stream_readable_concat
Open

stream: Readable.concat util method#40075
tugrul wants to merge 1 commit into
nodejs:mainfrom
tugrul:stream_readable_concat

Conversation

@tugrul

@tugrul tugrul commented Sep 10, 2021

Copy link
Copy Markdown
Contributor

There are many npm modules for this functionality and I think it is a common requirements because I see 452k weekly downloads of multistream npm package.

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Sep 10, 2021
Comment thread doc/api/stream.md Outdated

### `stream.Readable.concat(streams, [options])`

* `streams` {Readable[]|Iterable[]|AsyncIterable[]} List of readable streams or iterator of readable stream generator

@VoltrexKeyva VoltrexKeyva Sep 10, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `streams` {Readable[]|Iterable[]|AsyncIterable[]} List of readable streams or iterator of readable stream generator
* `streams` {Readable[]|Iterable[]|AsyncIterable[]} List of readable streams or
iterator of readable stream generator.

Every line must be 80 characters or less in length and this line should end with a period.

Comment thread doc/api/stream.md Outdated
Comment on lines +2084 to +2085
By default, `Readable.concat()` will set `options.objectMode` to `false`, unless
this is explicitly opted out by setting `options.objectMode` to `true`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By default, `Readable.concat()` will set `options.objectMode` to `false`, unless
this is explicitly opted out by setting `options.objectMode` to `true`.
By default, `Readable.concat()` will set `options.objectMode` to `false`,
unless this is explicitly opted out by setting `options.objectMode` to `true`.

Every line must be 80 characters or less in length.

Comment thread doc/api/stream.md Outdated
Comment on lines +2088 to +2089
A utility method to concatenate multiple readable streams to single one. The main readable stream will consume the
list of readable streams in order

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A utility method to concatenate multiple readable streams to single one. The main readable stream will consume the
list of readable streams in order
A utility method to concatenate multiple readable streams to single one. The
main readable stream will consume the list of readable streams in order.

Every line must be 80 characters or less in length and this line must end with a period.

Comment thread lib/internal/streams/readable.js Outdated
}

Readable.concat = function(streams, opts) {
return Readable.from(streamSeriesIterator(streams), {objectMode: false, ...opts});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Readable.from(streamSeriesIterator(streams), {objectMode: false, ...opts});
return Readable.from(streamSeriesIterator(streams),
{ objectMode: false, ...opts });

Lint.

@VoltrexKeyva VoltrexKeyva added the stream Issues and PRs related to the stream subsystem. label Sep 10, 2021
Comment thread lib/internal/streams/readable.js Outdated
}
}

Readable.concat = function(streams, opts) {

@aduh95 aduh95 Sep 10, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make Readable.concat.length equal to 1:

Suggested change
Readable.concat = function(streams, opts) {
Readable.concat = function(streams, opts = undefined) {

Comment thread lib/internal/streams/readable.js Outdated
Comment on lines +1376 to +1378
for await (const chunk of stream) {
yield chunk;
}

@aduh95 aduh95 Sep 10, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that have the same effect?

Suggested change
for await (const chunk of stream) {
yield chunk;
}
yield* stream;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works fine and looks better. Thanks

@tugrul
tugrul force-pushed the stream_readable_concat branch 6 times, most recently from e03b91f to 41bb9f4 Compare September 11, 2021 00:23
@tniessen

Copy link
Copy Markdown
Member

cc @nodejs/streams

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

Would you mind to add a few unit test for this feature?

I think if one of the "waiting" streams errors all should be destroyed, similar to how pipeline works.

Comment thread lib/internal/streams/readable.js Outdated

Readable.concat = function(streams, opts = undefined) {
return Readable.from(streamSeriesIterator(streams),
{ objectMode: false, ...opts });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IbjectMode should be true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn’t support old streams. See pipeline for how we fix that.

@tugrul

tugrul commented Sep 11, 2021

Copy link
Copy Markdown
Contributor Author

@mcollina I quickly lookup and only we need destroy remaining readable streams if it is an Array when error occurs on previous readable readings. This control not necessary for iterables because stream will initialize lazy. We do not have control of writable stream in this scenario. Following solutions can solve the problem.

async function * streamSeriesIterator(streams) {
  let index = 0;

  try {
    for await (const stream of streams) {
      index++;
      yield* stream;
    }
  } finally {
    if (streams instanceof Array) {
      streams.slice(index).forEach(stream => stream.destroy());
    }
  }
}

or

async function * streamSeriesIterator(streams) {
  let index = 0;

  try {
    for await (const stream of streams) {
      index++;
      yield* stream;
    }
  } catch (err) {
    if (streams instanceof Array) {
      streams.slice(index).forEach(stream => stream.destroy(err));
    }
    
    throw err;
  }
}

@jasnell

jasnell commented Sep 11, 2021

Copy link
Copy Markdown
Member

As an alternative suggestion... consider adding this utility to the streams/consumers module and making it work with any stream.Readable, ReadableStream, and async iterable...

import { concat, arrayBuffer } from 'node:stream/consumers';

const readable1 = new ReadableStream();
const readable2 = new ReadableStream();
const readable3 = new stream.Readable();

for await (const chunk of concat(readable1, readable2, readable3)) {
  console.log(chunk);
}

// Let's say you want all of the contents in a single ArrayBuffer...

await arrayBuffer(concat(readable1, readable2, readable3));

The return value would be just an async iterator. If someone wanted a stream.Readable from the results, it would be as simple as... stream.Readable.from(concat(readable1, readable2, readable3)) ... getting a ReadableStream from it is a bit more complicated but also straightforward...

const readable = new ReadableStream({
  async pull(c) {
    for await (const chunk of concat(readable1, readable2, readable3))
      c.enqueue(chunk);
    c.close();
  }
});

@mcollina

Copy link
Copy Markdown
Member

@tugrul unfortunately #40075 (comment) does not work for node streams. They can error at any time even if they are not being consumed, you need to add on('error') handlers.

@Ethan-Arrowood

Copy link
Copy Markdown
Contributor

Hello, I'm doing some backlog hygiene on all PRs related to networking and streaming modules. There hasn't been any activity here for quite a while so I'm going to mark it stalled. It will auto-close in ~30 days. This is not a rejection; if you've like to pick it back up, rebase onto latest main and give me a ping.

Thank you!

@Ethan-Arrowood Ethan-Arrowood added the stalled Issues and PRs that are stalled. label Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open.

@tugrul
tugrul force-pushed the stream_readable_concat branch from 41bb9f4 to 2e226ec Compare July 26, 2026 14:51
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 2e226ec to b2ab59a Compare July 26, 2026 15:23
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from b2ab59a to 9bf4fb7 Compare July 26, 2026 15:39
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 9bf4fb7 to 66e2a87 Compare July 26, 2026 16:22
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.73434% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.14%. Comparing base (4a5eb1c) to head (d019ad6).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
lib/stream/consumers/concat.js 93.71% 25 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #40075      +/-   ##
==========================================
- Coverage   90.15%   90.14%   -0.02%     
==========================================
  Files         744      745       +1     
  Lines      242517   242917     +400     
  Branches    45688    45782      +94     
==========================================
+ Hits       218642   218968     +326     
- Misses      15358    15431      +73     
- Partials     8517     8518       +1     
Files with missing lines Coverage Δ
lib/stream/consumers.js 100.00% <100.00%> (ø)
lib/stream/consumers/concat.js 93.71% <93.71%> (ø)

... and 31 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 66e2a87 to 32aeb80 Compare July 26, 2026 18:51
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 32aeb80 to 7fc582e Compare July 26, 2026 19:05
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 7fc582e to 1a78e41 Compare July 26, 2026 21:04
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 1a78e41 to 7891de2 Compare July 26, 2026 21:13
tugrul added a commit to tugrul/node that referenced this pull request Jul 26, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 7891de2 to 544f96c Compare July 26, 2026 22:10
@github-actions github-actions Bot removed the stalled Issues and PRs that are stalled. label Jul 27, 2026
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 544f96c to b3e3711 Compare July 27, 2026 10:47
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from b3e3711 to 4c4da65 Compare July 27, 2026 12:16
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 4c4da65 to 7d3fbad Compare July 27, 2026 13:10
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 7d3fbad to a9449d1 Compare July 27, 2026 15:26
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from a9449d1 to 82f0dd4 Compare July 27, 2026 15:52
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 82f0dd4 to 2097cec Compare July 27, 2026 16:34
tugrul added a commit to tugrul/node that referenced this pull request Jul 27, 2026
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from 2097cec to c10dd5f Compare July 27, 2026 18:36
Adds stream.concat(sources[, options]), which reads a sequence of
readable sources as a single async iterator. Each source is consumed
to completion before the next is started.

Returning an iterator rather than a Readable avoids having to pick an
objectMode on the caller's behalf: a concatenation of byte streams and
one of object streams want opposite defaults, and only the sources
know which case applies. Callers that want a stream can wrap the
result with Readable.from().

Sources are guarded as they are acquired rather than as they are read.
A Readable is live from construction, so a queued source that fails
while an earlier one is still draining would otherwise emit 'error'
with no listener attached and take down the process. concat() attaches
a handler to every source it holds a reference to, reports the first
failure, and destroys the rest.

Because a collection that already holds live streams and a producer
that creates one per iteration are indistinguishable, the input
protocol selects the behavior: a sync iterable is drained and guarded
up front, an async iterable is pulled one source at a time. Factories
stay inert until reached, and options.lazy overrides the default where
it guesses wrong.

Refs: nodejs#40075
Assisted-By: Claude/Opus 5
Signed-off-by: Tuğrul Topuz <tugrultopuz@gmail.com>
@tugrul
tugrul force-pushed the stream_readable_concat branch from c10dd5f to d019ad6 Compare July 27, 2026 19:02
@tugrul
tugrul requested a review from mcollina July 27, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants