mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Reject Writer.end() when its signal is already aborted without closing the writer. For push writers, reject the pending operation if the signal aborts while buffered data drains, while allowing the graceful close to continue. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol PR-URL: https://github.com/nodejs/node/pull/64727 Fixes: https://github.com/nodejs/node/issues/64726 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
206 lines
5.3 KiB
JavaScript
206 lines
5.3 KiB
JavaScript
// Flags: --experimental-stream-iter
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const { duplex, text, bytes } = require('stream/iter');
|
|
|
|
// =============================================================================
|
|
// Basic duplex
|
|
// =============================================================================
|
|
|
|
async function testBasicDuplex() {
|
|
const [channelA, channelB] = duplex();
|
|
|
|
// A writes, B reads
|
|
await channelA.writer.write('hello from A');
|
|
await channelA.close();
|
|
|
|
const dataAtB = await text(channelB.readable);
|
|
assert.strictEqual(dataAtB, 'hello from A');
|
|
}
|
|
|
|
async function testBidirectional() {
|
|
const [channelA, channelB] = duplex();
|
|
|
|
// A writes to B, B writes to A concurrently
|
|
const writeA = (async () => {
|
|
await channelA.writer.write('A to B');
|
|
await channelA.close();
|
|
})();
|
|
|
|
const writeB = (async () => {
|
|
await channelB.writer.write('B to A');
|
|
await channelB.close();
|
|
})();
|
|
|
|
const readAtB = text(channelB.readable);
|
|
const readAtA = text(channelA.readable);
|
|
|
|
await Promise.all([writeA, writeB]);
|
|
|
|
const [dataAtA, dataAtB] = await Promise.all([readAtA, readAtB]);
|
|
|
|
assert.strictEqual(dataAtB, 'A to B');
|
|
assert.strictEqual(dataAtA, 'B to A');
|
|
}
|
|
|
|
async function testMultipleWrites() {
|
|
const [channelA, channelB] = duplex({ budget: 16384 });
|
|
|
|
await channelA.writer.write('one');
|
|
await channelA.writer.write('two');
|
|
await channelA.writer.write('three');
|
|
await channelA.close();
|
|
|
|
const data = await text(channelB.readable);
|
|
assert.strictEqual(data, 'onetwothree');
|
|
}
|
|
|
|
async function testChannelClose() {
|
|
const [channelA, channelB] = duplex();
|
|
|
|
await channelA.close();
|
|
|
|
// Should be able to close twice without error
|
|
await channelA.close();
|
|
|
|
// B's readable should end (A -> B direction is closed)
|
|
const batches = [];
|
|
for await (const batch of channelB.readable) {
|
|
batches.push(batch);
|
|
}
|
|
assert.strictEqual(batches.length, 0);
|
|
}
|
|
|
|
async function testWithOptions() {
|
|
const [channelA, channelB] = duplex({
|
|
budget: 16384,
|
|
backpressure: 'strict',
|
|
});
|
|
|
|
await channelA.writer.write('msg');
|
|
await channelA.close();
|
|
|
|
const data = await text(channelB.readable);
|
|
assert.strictEqual(data, 'msg');
|
|
}
|
|
|
|
async function testPerChannelOptions() {
|
|
const [channelA, channelB] = duplex({
|
|
a: { budget: 16384 },
|
|
b: { budget: 16384 },
|
|
});
|
|
|
|
// Channel A -> B direction uses A's options
|
|
// Channel B -> A direction uses B's options
|
|
await channelA.writer.write('from-a');
|
|
await channelA.close();
|
|
|
|
await channelB.writer.write('from-b');
|
|
await channelB.close();
|
|
|
|
const [dataAtA, dataAtB] = await Promise.all([
|
|
text(channelA.readable),
|
|
text(channelB.readable),
|
|
]);
|
|
|
|
assert.strictEqual(dataAtB, 'from-a');
|
|
assert.strictEqual(dataAtA, 'from-b');
|
|
}
|
|
|
|
async function testAbortSignal() {
|
|
const ac = new AbortController();
|
|
const [channelA] = duplex({ signal: ac.signal });
|
|
|
|
ac.abort();
|
|
|
|
// Both directions should error
|
|
await assert.rejects(
|
|
async () => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for await (const _ of channelA.readable) {
|
|
assert.fail('Should not reach here');
|
|
}
|
|
},
|
|
(err) => err.name === 'AbortError',
|
|
);
|
|
}
|
|
|
|
async function testWriterEndWithPreAbortedSignal() {
|
|
const [channelA, channelB] = duplex();
|
|
const reason = new Error('end aborted');
|
|
|
|
await assert.rejects(
|
|
channelA.writer.end({ signal: AbortSignal.abort(reason) }),
|
|
(error) => error === reason,
|
|
);
|
|
|
|
await channelA.writer.write('still open');
|
|
const completedEnd = channelA.writer.end();
|
|
assert.strictEqual(await text(channelB.readable), 'still open');
|
|
assert.strictEqual(await completedEnd, 10);
|
|
await channelB.close();
|
|
}
|
|
|
|
async function testEmptyDuplex() {
|
|
const [channelA, channelB] = duplex();
|
|
|
|
// Close without writing
|
|
await channelA.close();
|
|
await channelB.close();
|
|
|
|
const dataAtA = await bytes(channelA.readable);
|
|
const dataAtB = await bytes(channelB.readable);
|
|
|
|
assert.strictEqual(dataAtA.byteLength, 0);
|
|
assert.strictEqual(dataAtB.byteLength, 0);
|
|
}
|
|
|
|
// Channel fail propagation
|
|
async function testChannelFail() {
|
|
const [a, b] = duplex();
|
|
a.writer.fail(new Error('channel failed'));
|
|
await assert.rejects(async () => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for await (const _ of b.readable) { /* consume */ }
|
|
}, { message: 'channel failed' });
|
|
await b.close();
|
|
}
|
|
|
|
// Abort signal affects both channels
|
|
async function testAbortSignalBothChannels() {
|
|
const ac = new AbortController();
|
|
const [channelA, channelB] = duplex({ signal: ac.signal });
|
|
|
|
ac.abort();
|
|
|
|
await assert.rejects(async () => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for await (const _ of channelA.readable) {
|
|
assert.fail('Should not reach here');
|
|
}
|
|
}, (err) => err.name === 'AbortError');
|
|
|
|
await assert.rejects(async () => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for await (const _ of channelB.readable) {
|
|
assert.fail('Should not reach here');
|
|
}
|
|
}, (err) => err.name === 'AbortError');
|
|
}
|
|
|
|
Promise.all([
|
|
testBasicDuplex(),
|
|
testBidirectional(),
|
|
testMultipleWrites(),
|
|
testChannelClose(),
|
|
testWithOptions(),
|
|
testPerChannelOptions(),
|
|
testAbortSignal(),
|
|
testWriterEndWithPreAbortedSignal(),
|
|
testEmptyDuplex(),
|
|
testChannelFail(),
|
|
testAbortSignalBothChannels(),
|
|
]).then(common.mustCall());
|