Skip to content

Commit 0032189

Browse files
authored
stream: avoid draining merged iter sources
Defer reading the next value from each source in merge() until the merged consumer resumes after receiving the previous value. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #64293 Fixes: #63566 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent db7cc5b commit 0032189

2 files changed

Lines changed: 44 additions & 15 deletions

File tree

lib/internal/streams/iter/consumers.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -463,20 +463,11 @@ function merge(...args) {
463463
if (result.done) {
464464
activeCount--;
465465
} else {
466-
ArrayPrototypePush(ready, result.value);
467-
// Immediately request the next value from this source
468-
// (at most one pending .next() per source)
469-
PromisePrototypeThen(
470-
iterator.next(),
471-
(r) => onSettled(iterator, r),
472-
(err) => {
473-
ArrayPrototypePush(ready, { __proto__: null, error: err });
474-
if (waitResolve) {
475-
waitResolve();
476-
waitResolve = null;
477-
}
478-
},
479-
);
466+
ArrayPrototypePush(ready, {
467+
__proto__: null,
468+
iterator,
469+
value: result.value,
470+
});
480471
}
481472
if (waitResolve) {
482473
waitResolve();
@@ -513,7 +504,18 @@ function merge(...args) {
513504
if (item?.error) {
514505
throw item.error;
515506
}
516-
yield item;
507+
yield item.value;
508+
PromisePrototypeThen(
509+
item.iterator.next(),
510+
(r) => onSettled(item.iterator, r),
511+
(err) => {
512+
ArrayPrototypePush(ready, { __proto__: null, error: err });
513+
if (waitResolve) {
514+
waitResolve();
515+
waitResolve = null;
516+
}
517+
},
518+
);
517519
}
518520

519521
// If sources are still active, wait for the next settlement

test/parallel/test-stream-iter-consumers-merge.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,32 @@ async function testMergeSignalDuringPendingMultiSourceRead() {
170170
await assert.rejects(next, { name: 'AbortError' });
171171
}
172172

173+
async function testMergeDoesNotDrainSourcesWhileIdle() {
174+
function source(n) {
175+
return {
176+
__proto__: null,
177+
pulls: 0,
178+
async *[Symbol.asyncIterator]() {
179+
while (this.pulls < n) {
180+
yield [Buffer.from(`${++this.pulls}`)];
181+
}
182+
},
183+
};
184+
}
185+
186+
const a = source(5);
187+
const b = source(5);
188+
const iterator = merge(a, b)[Symbol.asyncIterator]();
189+
190+
await iterator.next();
191+
await new Promise(setImmediate);
192+
193+
assert.strictEqual(a.pulls, 1);
194+
assert.strictEqual(b.pulls, 1);
195+
196+
await iterator.return?.();
197+
}
198+
173199
// merge() accepts string sources (normalized via from())
174200
async function testMergeStringSources() {
175201
const batches = [];
@@ -306,6 +332,7 @@ Promise.all([
306332
testMergeConsumerBreak(),
307333
testMergeSignalMidIteration(),
308334
testMergeSignalDuringPendingMultiSourceRead(),
335+
testMergeDoesNotDrainSourcesWhileIdle(),
309336
testMergeStringSources(),
310337
testMergeObjectLikeSources(),
311338
testMergeCleanupErrorOnly(),

0 commit comments

Comments
 (0)