Skip to content

Commit 535f934

Browse files
committed
stream: fixup writer to terminate on consumer return/throw
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #65652 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent ae168ea commit 535f934

2 files changed

Lines changed: 47 additions & 19 deletions

File tree

lib/internal/streams/iter/push.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,13 @@ class PushQueue {
425425
// ===========================================================================
426426

427427
async read() {
428+
if (this.#consumerState === 'returned') {
429+
return { __proto__: null, done: true, value: undefined };
430+
}
431+
if (this.#consumerState === 'thrown') {
432+
throw this.#error;
433+
}
434+
428435
// If there's data in the buffer, return it immediately
429436
if (this.#slots.length > 0) {
430437
const result = this.#drain();
@@ -458,16 +465,9 @@ class PushQueue {
458465
consumerReturn() {
459466
if (this.#consumerState !== 'active') return;
460467
this.#consumerState = 'returned';
461-
this.#cleanup();
468+
const error = new ERR_INVALID_STATE.TypeError('Stream closed by consumer');
469+
this.#terminateWriterFromConsumer(error);
462470
this.#resolvePendingReads();
463-
this.#rejectPendingWrites(
464-
new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
465-
// If closing, reject the pending end promise
466-
if (this.#writerState === 'closing' && this.#pendingEnd) {
467-
this.#pendingEnd.reject(
468-
new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
469-
this.#pendingEnd = null;
470-
}
471471
// Resolve pending drains with false - no more data will be consumed
472472
this.#resolvePendingDrains(false);
473473
}
@@ -476,13 +476,8 @@ class PushQueue {
476476
if (this.#consumerState !== 'active') return;
477477
this.#consumerState = 'thrown';
478478
this.#error = error;
479-
this.#cleanup();
479+
this.#terminateWriterFromConsumer(error);
480480
this.#rejectPendingReads(error);
481-
this.#rejectPendingWrites(error);
482-
if (this.#writerState === 'closing' && this.#pendingEnd) {
483-
this.#pendingEnd.reject(error);
484-
this.#pendingEnd = null;
485-
}
486481
// Reject pending drains - the consumer errored
487482
this.#rejectPendingDrains(error);
488483
}
@@ -516,9 +511,30 @@ class PushQueue {
516511
return size;
517512
}
518513

514+
#terminateWriterFromConsumer(error) {
515+
this.#slots.clear();
516+
this.#bufferedBytes = 0;
517+
if (this.#writerState === 'open' || this.#writerState === 'closing') {
518+
this.#writerState = 'errored';
519+
this.#error = error;
520+
}
521+
this.#cleanup();
522+
this.#rejectPendingWrites(error);
523+
if (this.#pendingEnd) {
524+
this.#pendingEnd.reject(error);
525+
this.#pendingEnd = null;
526+
}
527+
}
528+
519529
#resolvePendingReads() {
520530
while (this.#pendingReads.length > 0) {
521-
if (this.#slots.length > 0) {
531+
if (this.#consumerState === 'returned') {
532+
const pending = this.#pendingReads.shift();
533+
pending.resolve({ __proto__: null, done: true, value: undefined });
534+
} else if (this.#consumerState === 'thrown') {
535+
const pending = this.#pendingReads.shift();
536+
pending.reject(this.#error);
537+
} else if (this.#slots.length > 0) {
522538
const pending = this.#pendingReads.shift();
523539
const result = this.#drain();
524540
this.#resolvePendingWrites();
@@ -533,9 +549,6 @@ class PushQueue {
533549
} else if (this.#writerState === 'errored') {
534550
const pending = this.#pendingReads.shift();
535551
pending.reject(this.#error);
536-
} else if (this.#consumerState === 'returned') {
537-
const pending = this.#pendingReads.shift();
538-
pending.resolve({ __proto__: null, done: true, value: undefined });
539552
} else {
540553
break;
541554
}

test/parallel/test-stream-iter-push-writer.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,20 @@ async function testConsumerReturnResolvesPendingRead() {
424424
assert.strictEqual(readResult.done, true);
425425
}
426426

427+
async function testEndRejectsAfterConsumerReturn() {
428+
const { writer, readable } = push();
429+
writer.writeSync('data');
430+
const iter = readable[Symbol.asyncIterator]();
431+
432+
await iter.return();
433+
434+
await assert.rejects(
435+
writer.end({ signal: AbortSignal.timeout(common.platformTimeout(100)) }),
436+
{ code: 'ERR_INVALID_STATE' },
437+
);
438+
assert.strictEqual((await iter.next()).done, true);
439+
}
440+
427441
// iterator.throw() rejects a pending read with the thrown error
428442
async function testConsumerThrowRejectsPendingRead() {
429443
const { readable } = push();
@@ -599,6 +613,7 @@ Promise.all([
599613
testFailRejectsFutureReadWithFalsyReason(),
600614
testFailRejectsPendingReadWithFalsyReason(),
601615
testConsumerReturnResolvesPendingRead(),
616+
testEndRejectsAfterConsumerReturn(),
602617
testConsumerThrowRejectsPendingRead(),
603618
testEndRejectsPendingWrites(),
604619
testEndIdempotentWhenClosed(),

0 commit comments

Comments
 (0)