Skip to content

Commit 7bee754

Browse files
authored
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> PR-URL: #64445 Fixes: #64444 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent e383a1d commit 7bee754

3 files changed

Lines changed: 40 additions & 3 deletions

File tree

lib/internal/streams/iter/consumers.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,10 @@ function merge(...args) {
426426
if (normalized.length === 0) return;
427427

428428
if (normalized.length === 1) {
429-
for await (const batch of normalized[0]) {
430-
signal?.throwIfAborted();
429+
const source = signal !== undefined && isAsyncIterable(sources[0]) ?
430+
from(yieldAbortable(sources[0], signal)) :
431+
yieldAbortable(normalized[0], signal);
432+
for await (const batch of source) {
431433
yield batch;
432434
}
433435
return;

lib/internal/streams/iter/utils.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ function yieldAbortable(source, signal) {
133133
if (!completed && typeof iterator.return === 'function') {
134134
const result = iterator.return();
135135
if (aborted) {
136-
markPromiseAsHandled(result);
136+
// PromiseResolve(result) can reject if result is a thenable that
137+
// rejects, so mark it as handled even though the abort takes
138+
// precedence over the result of iterator.return().
139+
markPromiseAsHandled(PromiseResolve(result));
137140
} else {
138141
await result;
139142
}

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

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

173+
async function testMergeSignalDuringPendingSingleSourceRead() {
174+
const ac = new AbortController();
175+
let returned = false;
176+
const source = {
177+
__proto__: null,
178+
[Symbol.asyncIterator]() {
179+
return this;
180+
},
181+
next() {
182+
// Intentionally never settle to verify that aborting interrupts a pending read.
183+
return new Promise(() => {});
184+
},
185+
return() {
186+
returned = true;
187+
return { __proto__: null, done: true };
188+
},
189+
};
190+
191+
const iter = merge(source, {
192+
__proto__: null,
193+
signal: ac.signal,
194+
})[Symbol.asyncIterator]();
195+
196+
const next = iter.next();
197+
await new Promise(setImmediate);
198+
ac.abort();
199+
200+
await assert.rejects(next, { name: 'AbortError' });
201+
assert.strictEqual(returned, true);
202+
}
203+
173204
async function testMergeDoesNotDrainSourcesWhileIdle() {
174205
function source(n) {
175206
return {
@@ -332,6 +363,7 @@ Promise.all([
332363
testMergeConsumerBreak(),
333364
testMergeSignalMidIteration(),
334365
testMergeSignalDuringPendingMultiSourceRead(),
366+
testMergeSignalDuringPendingSingleSourceRead(),
335367
testMergeDoesNotDrainSourcesWhileIdle(),
336368
testMergeStringSources(),
337369
testMergeObjectLikeSources(),

0 commit comments

Comments
 (0)