From 1ed237919bdafe89ec24c40df2d621035d5ae1a9 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:12:57 -0700 Subject: [PATCH 1/3] stream: abort pending single-source merge reads Make the single-source merge path abort-aware so signal cancellation rejects a pending read and closes the active source iterator. Handle synchronous iterator return values when marking aborted cleanup as handled. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> --- lib/internal/streams/iter/consumers.js | 6 ++-- lib/internal/streams/iter/utils.js | 2 +- .../test-stream-iter-consumers-merge.js | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/internal/streams/iter/consumers.js b/lib/internal/streams/iter/consumers.js index 8e18c1b5b930ea..3894b3fa24a8c4 100644 --- a/lib/internal/streams/iter/consumers.js +++ b/lib/internal/streams/iter/consumers.js @@ -426,8 +426,10 @@ function merge(...args) { if (normalized.length === 0) return; if (normalized.length === 1) { - for await (const batch of normalized[0]) { - signal?.throwIfAborted(); + const source = signal !== undefined && isAsyncIterable(sources[0]) ? + from(yieldAbortable(sources[0], signal)) : + yieldAbortable(normalized[0], signal); + for await (const batch of source) { yield batch; } return; diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js index ec02eca43f3385..e3ad2d0ca229b7 100644 --- a/lib/internal/streams/iter/utils.js +++ b/lib/internal/streams/iter/utils.js @@ -142,7 +142,7 @@ function yieldAbortable(source, signal) { if (!completed && typeof iterator.return === 'function') { const result = iterator.return(); if (aborted) { - markPromiseAsHandled(result); + markPromiseAsHandled(PromiseResolve(result)); } else { await result; } diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js index 859fcf8c1580fa..f0be32c2e52a54 100644 --- a/test/parallel/test-stream-iter-consumers-merge.js +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -170,6 +170,36 @@ async function testMergeSignalDuringPendingMultiSourceRead() { await assert.rejects(next, { name: 'AbortError' }); } +async function testMergeSignalDuringPendingSingleSourceRead() { + const ac = new AbortController(); + let returned = false; + const source = { + __proto__: null, + [Symbol.asyncIterator]() { + return this; + }, + next() { + return new Promise(() => {}); + }, + return() { + returned = true; + return { __proto__: null, done: true }; + }, + }; + + const iter = merge(source, { + __proto__: null, + signal: ac.signal, + })[Symbol.asyncIterator](); + + const next = iter.next(); + await new Promise(setImmediate); + ac.abort(); + + await assert.rejects(next, { name: 'AbortError' }); + assert.strictEqual(returned, true); +} + async function testMergeDoesNotDrainSourcesWhileIdle() { function source(n) { return { @@ -332,6 +362,7 @@ Promise.all([ testMergeConsumerBreak(), testMergeSignalMidIteration(), testMergeSignalDuringPendingMultiSourceRead(), + testMergeSignalDuringPendingSingleSourceRead(), testMergeDoesNotDrainSourcesWhileIdle(), testMergeStringSources(), testMergeObjectLikeSources(), From 62c9f4f2a691b8e0c551571b61cf8271d883d790 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sun, 12 Jul 2026 09:26:37 -0700 Subject: [PATCH 2/3] fixup! stream: explain handling of rejecting iterator return thenables --- lib/internal/streams/iter/utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/internal/streams/iter/utils.js b/lib/internal/streams/iter/utils.js index e3ad2d0ca229b7..c86f317f490015 100644 --- a/lib/internal/streams/iter/utils.js +++ b/lib/internal/streams/iter/utils.js @@ -142,6 +142,9 @@ function yieldAbortable(source, signal) { if (!completed && typeof iterator.return === 'function') { const result = iterator.return(); if (aborted) { + // PromiseResolve(result) can reject if result is a thenable that + // rejects, so mark it as handled even though the abort takes + // precedence over the result of iterator.return(). markPromiseAsHandled(PromiseResolve(result)); } else { await result; From 1ee6017dda72d4c6bcab0788fc5043ea2aa14d13 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Sun, 12 Jul 2026 09:28:49 -0700 Subject: [PATCH 3/3] fixup! test: explain intentionally pending promise in stream merge test --- test/parallel/test-stream-iter-consumers-merge.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js index f0be32c2e52a54..84aeb24b615915 100644 --- a/test/parallel/test-stream-iter-consumers-merge.js +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -179,6 +179,7 @@ async function testMergeSignalDuringPendingSingleSourceRead() { return this; }, next() { + // Intentionally never settle to verify that aborting interrupts a pending read. return new Promise(() => {}); }, return() {