Skip to content

Commit caa5a1b

Browse files
author
Nitzan Uziely
committed
stream: add AbortSignal support to finished
Add AbortSignal support to stream.finished
1 parent 88d9268 commit caa5a1b

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

doc/api/stream.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,9 @@ further errors except from `_destroy()` may be emitted as `'error'`.
15781578
<!-- YAML
15791579
added: v10.0.0
15801580
changes:
1581+
- version: REPLACEME
1582+
pr-url: https://github.com/nodejs/node/pull/37354
1583+
description: The `signal` option was added.
15811584
- version: v14.0.0
15821585
pr-url: https://github.com/nodejs/node/pull/32158
15831586
description: The `finished(stream, cb)` will wait for the `'close'` event
@@ -1604,6 +1607,9 @@ changes:
16041607
* `writable` {boolean} When set to `false`, the callback will be called when
16051608
the stream ends even though the stream might still be writable.
16061609
**Default**: `true`.
1610+
* `signal` {AbortSignal} allows aborting the wait for the stream finish. The
1611+
underlying stream will *not* be aborted if the signal is aborted. The
1612+
callback will get called with an `AbortError`.
16071613
* `callback` {Function} A callback function that takes an optional error
16081614
argument.
16091615
* Returns: {Function} A cleanup function which removes all registered

lib/internal/streams/end-of-stream.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
const {
77
FunctionPrototype,
88
FunctionPrototypeCall,
9+
ReflectApply,
910
} = primordials;
11+
const {
12+
codes,
13+
AbortError,
14+
} = require('internal/errors');
1015
const {
1116
ERR_STREAM_PREMATURE_CLOSE
12-
} = require('internal/errors').codes;
17+
} = codes;
1318
const { once } = require('internal/util');
1419
const {
1520
validateFunction,
1621
validateObject,
22+
validateAbortSignal,
1723
} = require('internal/validators');
1824

1925
function isSocket(stream) {
@@ -76,6 +82,7 @@ function eos(stream, options, callback) {
7682
validateObject(options, 'options');
7783
}
7884
validateFunction(callback, 'callback');
85+
validateAbortSignal(options.signal, 'options.signal');
7986

8087
callback = once(callback);
8188

@@ -199,6 +206,20 @@ function eos(stream, options, callback) {
199206
});
200207
}
201208

209+
if (options.signal && !closed) {
210+
const abort = () => callback(new AbortError());
211+
if (options.signal.aborted) {
212+
process.nextTick(abort);
213+
} else {
214+
const originalCallback = callback;
215+
callback = once((...args) => {
216+
options.signal.removeEventListener('abort', abort);
217+
ReflectApply(originalCallback, null, args);
218+
});
219+
options.signal.addEventListener('abort', abort);
220+
}
221+
}
222+
202223
return function() {
203224
callback = nop;
204225
stream.removeListener('aborted', onclose);

test/parallel/test-stream-finished.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,89 @@ const http = require('http');
9292
run();
9393
}
9494

95+
{
96+
// Check pre-cancelled
97+
const signal = new EventTarget();
98+
signal.aborted = true;
99+
100+
const rs = Readable.from((function* () {})());
101+
finished(rs, { signal }, common.mustCall((err) => {
102+
assert.strictEqual(err.name, 'AbortError');
103+
}));
104+
}
105+
106+
{
107+
// Check cancelled before the stream ends sync.
108+
const ac = new AbortController();
109+
const { signal } = ac;
110+
111+
const rs = Readable.from((function* () {})());
112+
finished(rs, { signal }, common.mustCall((err) => {
113+
assert.strictEqual(err.name, 'AbortError');
114+
}));
115+
116+
ac.abort();
117+
}
118+
119+
{
120+
// Check cancelled before the stream ends async.
121+
const ac = new AbortController();
122+
const { signal } = ac;
123+
124+
const rs = Readable.from((function* () {})());
125+
setTimeout(() => ac.abort(), 1);
126+
finished(rs, { signal }, common.mustCall((err) => {
127+
assert.strictEqual(err.name, 'AbortError');
128+
}));
129+
}
130+
131+
{
132+
// Check cancelled after doesn't throw.
133+
const ac = new AbortController();
134+
const { signal } = ac;
135+
136+
const rs = Readable.from((function* () {
137+
yield 5;
138+
setImmediate(() => ac.abort());
139+
})());
140+
rs.resume();
141+
finished(rs, { signal }, common.mustCall((err) => {
142+
assert.strictEqual(err, undefined);
143+
}));
144+
}
145+
146+
{
147+
// Promisified abort works
148+
const finishedPromise = promisify(finished);
149+
async function run() {
150+
const ac = new AbortController();
151+
const { signal } = ac;
152+
const rs = Readable.from((function* () {})());
153+
setImmediate(() => ac.abort());
154+
await finishedPromise(rs, { signal });
155+
}
156+
157+
run().catch(common.mustCall((err) => {
158+
assert.strictEqual(err.name, 'AbortError');
159+
}));
160+
}
161+
162+
{
163+
// Promisified pre-aborted works
164+
const finishedPromise = promisify(finished);
165+
async function run() {
166+
const signal = new EventTarget();
167+
signal.aborted = true;
168+
const rs = Readable.from((function* () {})());
169+
await finishedPromise(rs, { signal });
170+
}
171+
172+
run().catch(common.mustCall((err) => {
173+
assert.strictEqual(err.name, 'AbortError');
174+
}));
175+
}
176+
177+
95178
{
96179
const rs = fs.createReadStream('file-does-not-exist');
97180

0 commit comments

Comments
 (0)