Skip to content

Commit cee2067

Browse files
committed
stream: wait for push writer end fallback to drain
When endSync() returns -1 after buffered writes, a follow-up end() should stay pending until the readable side drains the queued data. Do not make duplex channel close() wait for that drain, since close() only needs to signal EOF to the peer. Waiting there can deadlock when the peer starts reading only after close() resolves. Fixes: #63502 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5
1 parent 8d3245e commit cee2067

3 files changed

Lines changed: 30 additions & 7 deletions

File tree

lib/internal/streams/iter/duplex.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ function duplex(options = { __proto__: null }) {
7474
if (aClosed) return;
7575
aClosed = true;
7676
// End the writer (signals end-of-stream to B's readable)
77-
if (aWriter.endSync() < 0) {
78-
await aWriter.end();
79-
}
77+
aWriter.endSync();
8078
// Stop iteration of this channel's readable
8179
if (aReadableIterator?.return) {
8280
await aReadableIterator.return();
@@ -104,9 +102,7 @@ function duplex(options = { __proto__: null }) {
104102
async close() {
105103
if (bClosed) return;
106104
bClosed = true;
107-
if (bWriter.endSync() < 0) {
108-
await bWriter.end();
109-
}
105+
bWriter.endSync();
110106
if (bReadableIterator?.return) {
111107
await bReadableIterator.return();
112108
bReadableIterator = null;

lib/internal/streams/iter/push.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,10 @@ class PushQueue {
274274
if (this.#writerState === 'errored') {
275275
return -2; // Signal to reject with stored error
276276
}
277-
if (this.#writerState === 'closing' || this.#writerState === 'closed') {
277+
if (this.#writerState === 'closing') {
278+
return -3; // Signal to PushWriter: wait for drain to complete
279+
}
280+
if (this.#writerState === 'closed') {
278281
return this.#bytesWritten; // Idempotent
279282
}
280283

@@ -636,6 +639,10 @@ class PushWriter {
636639
if (result === -3) {
637640
// Closing: buffer has data, create deferred promise that resolves
638641
// when consumer drains past the end sentinel
642+
const pendingEndPromise = this.#queue.pendingEndPromise;
643+
if (pendingEndPromise !== null) {
644+
return pendingEndPromise;
645+
}
639646
const { promise, resolve, reject } = PromiseWithResolvers();
640647
this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject });
641648
return promise;

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,25 @@ async function testEndAsyncReturnValue() {
232232
await consume;
233233
}
234234

235+
async function testEndAfterEndSyncWaitsForDrain() {
236+
const { writer, readable } = push();
237+
writer.writeSync('hello');
238+
assert.strictEqual(writer.endSync(), -1);
239+
240+
let ended = false;
241+
const end = writer.end().then((n) => {
242+
ended = true;
243+
return n;
244+
});
245+
246+
await Promise.resolve();
247+
assert.strictEqual(ended, false);
248+
249+
// eslint-disable-next-line no-unused-vars
250+
for await (const _ of readable) { /* drain */ }
251+
assert.strictEqual(await end, 5);
252+
}
253+
235254
async function testWriteUint8Array() {
236255
const { writer, readable } = push();
237256
writer.write(new Uint8Array([72, 73])); // 'HI'
@@ -413,6 +432,7 @@ Promise.all([
413432
testOndrainProtocolErrorPropagates(),
414433
testFail(),
415434
testEndAsyncReturnValue(),
435+
testEndAfterEndSyncWaitsForDrain(),
416436
testWriteUint8Array(),
417437
testOndrainWaitsForDrain(),
418438
testConsumerThrowRejectsWrites(),

0 commit comments

Comments
 (0)