Skip to content

Commit c7e55c6

Browse files
committed
stream: fix writable.end callback behavior
Changes so that the end() callback behaves the same way in relation to _final as write() does to _write/_writev. PR-URL: #34101 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent e2b468e commit c7e55c6

8 files changed

+40
-42
lines changed

doc/api/stream.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ Is `true` after [`writable.destroy()`][writable-destroy] has been called.
412412
<!-- YAML
413413
added: v0.9.4
414414
changes:
415+
- version: REPLACEME
416+
pr-url: https://github.com/nodejs/node/pull/34101
417+
description: The `callback` is invoked before 'finish' or on error.
415418
- version: v14.0.0
416419
pr-url: https://github.com/nodejs/node/pull/29747
417420
description: The `callback` is invoked if 'finish' or 'error' is emitted.
@@ -428,15 +431,13 @@ changes:
428431
`Uint8Array`. For object mode streams, `chunk` may be any JavaScript value
429432
other than `null`.
430433
* `encoding` {string} The encoding if `chunk` is a string
431-
* `callback` {Function} Optional callback for when the stream finishes
432-
or errors
434+
* `callback` {Function} Callback for when the stream is finished.
433435
* Returns: {this}
434436

435437
Calling the `writable.end()` method signals that no more data will be written
436438
to the [`Writable`][]. The optional `chunk` and `encoding` arguments allow one
437439
final additional chunk of data to be written immediately before closing the
438-
stream. If provided, the optional `callback` function is attached as a listener
439-
for the [`'finish'`][] and the `'error'` event.
440+
stream.
440441

441442
Calling the [`stream.write()`][stream-write] method after calling
442443
[`stream.end()`][stream-end] will raise an error.
@@ -592,7 +593,7 @@ changes:
592593
`Uint8Array`. For object mode streams, `chunk` may be any JavaScript value
593594
other than `null`.
594595
* `encoding` {string} The encoding, if `chunk` is a string. **Default:** `'utf8'`
595-
* `callback` {Function} Callback for when this chunk of data is flushed
596+
* `callback` {Function} Callback for when this chunk of data is flushed.
596597
* Returns: {boolean} `false` if the stream wishes for the calling code to
597598
wait for the `'drain'` event to be emitted before continuing to write
598599
additional data; otherwise `true`.

lib/_stream_writable.js

+25-28
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ ObjectSetPrototypeOf(Writable, Stream);
6464

6565
function nop() {}
6666

67+
const kOnFinished = Symbol('kOnFinished');
68+
6769
function WritableState(options, stream, isDuplex) {
6870
// Duplex streams are both readable and writable, but share
6971
// the same options object.
@@ -185,6 +187,8 @@ function WritableState(options, stream, isDuplex) {
185187
// True if close has been emitted or would have been emitted
186188
// depending on emitClose.
187189
this.closeEmitted = false;
190+
191+
this[kOnFinished] = [];
188192
}
189193

190194
function resetBuffer(state) {
@@ -411,7 +415,7 @@ function onwriteError(stream, state, er, cb) {
411415
// not enabled. Passing `er` here doesn't make sense since
412416
// it's related to one specific write, not to the buffered
413417
// writes.
414-
errorBuffer(state, new ERR_STREAM_DESTROYED('write'));
418+
errorBuffer(state);
415419
// This can emit error, but error must always follow cb.
416420
errorOrDestroy(stream, er);
417421
}
@@ -487,14 +491,14 @@ function afterWrite(stream, state, count, cb) {
487491
}
488492

489493
if (state.destroyed) {
490-
errorBuffer(state, new ERR_STREAM_DESTROYED('write'));
494+
errorBuffer(state);
491495
}
492496

493497
finishMaybe(stream, state);
494498
}
495499

496500
// If there's something in the buffer waiting, then invoke callbacks.
497-
function errorBuffer(state, err) {
501+
function errorBuffer(state) {
498502
if (state.writing) {
499503
return;
500504
}
@@ -503,7 +507,11 @@ function errorBuffer(state, err) {
503507
const { chunk, callback } = state.buffered[n];
504508
const len = state.objectMode ? 1 : chunk.length;
505509
state.length -= len;
506-
callback(err);
510+
callback(new ERR_STREAM_DESTROYED('write'));
511+
}
512+
513+
for (const callback of state[kOnFinished].splice(0)) {
514+
callback(new ERR_STREAM_DESTROYED('end'));
507515
}
508516

509517
resetBuffer(state);
@@ -611,10 +619,11 @@ Writable.prototype.end = function(chunk, encoding, cb) {
611619
}
612620

613621
if (typeof cb === 'function') {
614-
if (err || state.finished)
622+
if (err || state.finished) {
615623
process.nextTick(cb, err);
616-
else
617-
onFinished(this, cb);
624+
} else {
625+
state[kOnFinished].push(cb);
626+
}
618627
}
619628

620629
return this;
@@ -636,6 +645,9 @@ function callFinal(stream, state) {
636645
stream._final((err) => {
637646
state.pendingcb--;
638647
if (err) {
648+
for (const callback of state[kOnFinished].splice(0)) {
649+
callback(err);
650+
}
639651
errorOrDestroy(stream, err, state.sync);
640652
} else if (needFinish(state)) {
641653
state.prefinished = true;
@@ -683,6 +695,11 @@ function finish(stream, state) {
683695
return;
684696

685697
state.finished = true;
698+
699+
for (const callback of state[kOnFinished].splice(0)) {
700+
callback();
701+
}
702+
686703
stream.emit('finish');
687704

688705
if (state.autoDestroy) {
@@ -701,26 +718,6 @@ function finish(stream, state) {
701718
}
702719
}
703720

704-
// TODO(ronag): Avoid using events to implement internal logic.
705-
function onFinished(stream, cb) {
706-
function onerror(err) {
707-
stream.removeListener('finish', onfinish);
708-
stream.removeListener('error', onerror);
709-
cb(err);
710-
if (stream.listenerCount('error') === 0) {
711-
stream.emit('error', err);
712-
}
713-
}
714-
715-
function onfinish() {
716-
stream.removeListener('finish', onfinish);
717-
stream.removeListener('error', onerror);
718-
cb();
719-
}
720-
stream.on('finish', onfinish);
721-
stream.prependListener('error', onerror);
722-
}
723-
724721
ObjectDefineProperties(Writable.prototype, {
725722

726723
destroyed: {
@@ -800,7 +797,7 @@ const destroy = destroyImpl.destroy;
800797
Writable.prototype.destroy = function(err, cb) {
801798
const state = this._writableState;
802799
if (!state.destroyed) {
803-
process.nextTick(errorBuffer, state, new ERR_STREAM_DESTROYED('write'));
800+
process.nextTick(errorBuffer, state);
804801
}
805802
destroy.call(this, err, cb);
806803
return this;

test/parallel/test-stream-transform-final-sync.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const t = new stream.Transform({
9090
t.on('finish', common.mustCall(function() {
9191
state++;
9292
// finishListener
93-
assert.strictEqual(state, 14);
93+
assert.strictEqual(state, 15);
9494
}, 1));
9595
t.on('end', common.mustCall(function() {
9696
state++;
@@ -106,5 +106,5 @@ t.write(4);
106106
t.end(7, common.mustCall(function() {
107107
state++;
108108
// endMethodCallback
109-
assert.strictEqual(state, 15);
109+
assert.strictEqual(state, 14);
110110
}, 1));

test/parallel/test-stream-transform-final.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const t = new stream.Transform({
9292
t.on('finish', common.mustCall(function() {
9393
state++;
9494
// finishListener
95-
assert.strictEqual(state, 14);
95+
assert.strictEqual(state, 15);
9696
}, 1));
9797
t.on('end', common.mustCall(function() {
9898
state++;
@@ -108,5 +108,5 @@ t.write(4);
108108
t.end(7, common.mustCall(function() {
109109
state++;
110110
// endMethodCallback
111-
assert.strictEqual(state, 15);
111+
assert.strictEqual(state, 14);
112112
}, 1));

test/parallel/test-stream-writable-destroy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ const assert = require('assert');
354354
assert.strictEqual(err.message, 'asd');
355355
}));
356356
write.end('asd', common.mustCall((err) => {
357-
assert.strictEqual(err.message, 'asd');
357+
assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED');
358358
}));
359359
write.destroy(new Error('asd'));
360360
}

test/parallel/test-stream-writable-end-cb-error.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ const stream = require('stream');
1717
}));
1818
writable.write('asd');
1919
writable.end(common.mustCall((err) => {
20-
assert.strictEqual(err.message, 'kaboom');
20+
assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED');
2121
}));
2222
writable.end(common.mustCall((err) => {
23-
assert.strictEqual(err.message, 'kaboom');
23+
assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED');
2424
}));
2525
}
2626

test/parallel/test-stream-writable-end-cb-uncaught.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ writable._final = (cb) => {
1919

2020
writable.write('asd');
2121
writable.end(common.mustCall((err) => {
22-
assert.strictEqual(err.message, 'kaboom');
22+
assert.strictEqual(err.code, 'ERR_STREAM_DESTROYED');
2323
}));

test/parallel/test-stream-write-destroy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ for (const withPendingData of [ false, true ]) {
5757
w.destroy();
5858
assert.strictEqual(chunksWritten, 1);
5959
callbacks.shift()();
60-
assert.strictEqual(chunksWritten, 2);
60+
assert.strictEqual(chunksWritten, useEnd && !withPendingData ? 1 : 2);
6161
assert.strictEqual(callbacks.length, 0);
6262
assert.strictEqual(drains, 1);
6363

0 commit comments

Comments
 (0)