'use strict'; const common = require('../common'); const { Writable, Readable, Transform, finished, Duplex, PassThrough, Stream, } = require('stream'); const assert = require('assert'); const EE = require('events'); const fs = require('fs'); const { promisify } = require('util'); const http = require('http'); { const rs = new Readable({ read() {} }); finished(rs, common.mustSucceed()); rs.push(null); rs.resume(); } { const ws = new Writable({ write(data, enc, cb) { cb(); } }); finished(ws, common.mustSucceed()); ws.end(); } { const tr = new Transform({ transform(data, enc, cb) { cb(); } }); let finish = false; let ended = false; tr.on('end', () => { ended = true; }); tr.on('finish', () => { finish = true; }); finished(tr, common.mustSucceed(() => { assert(finish); assert(ended); })); tr.end(); tr.resume(); } { const rs = fs.createReadStream(__filename); rs.resume(); finished(rs, common.mustCall()); } { const finishedPromise = promisify(finished); async function run() { const rs = fs.createReadStream(__filename); const done = common.mustCall(); let ended = false; rs.resume(); rs.on('end', () => { ended = true; }); await finishedPromise(rs); assert(ended); done(); } run(); } { // Check pre-cancelled const signal = new EventTarget(); signal.aborted = true; const rs = Readable.from((function* () {})()); finished(rs, { signal }, common.mustCall((err) => { assert.strictEqual(err.name, 'AbortError'); })); } { // Check cancelled before the stream ends sync. const ac = new AbortController(); const { signal } = ac; const rs = Readable.from((function* () {})()); finished(rs, { signal }, common.mustCall((err) => { assert.strictEqual(err.name, 'AbortError'); })); ac.abort(); } { // Check cancelled before the stream ends async. const ac = new AbortController(); const { signal } = ac; const rs = Readable.from((function* () {})()); setTimeout(() => ac.abort(), 1); finished(rs, { signal }, common.mustCall((err) => { assert.strictEqual(err.name, 'AbortError'); })); } { // Check cancelled after doesn't throw. const ac = new AbortController(); const { signal } = ac; const rs = Readable.from((function* () { yield 5; setImmediate(() => ac.abort()); })()); rs.resume(); finished(rs, { signal }, common.mustSucceed()); } { // Promisified abort works const finishedPromise = promisify(finished); async function run() { const ac = new AbortController(); const { signal } = ac; const rs = Readable.from((function* () {})()); setImmediate(() => ac.abort()); await finishedPromise(rs, { signal }); } assert.rejects(run, { name: 'AbortError' }).then(common.mustCall()); } { // Promisified pre-aborted works const finishedPromise = promisify(finished); async function run() { const signal = new EventTarget(); signal.aborted = true; const rs = Readable.from((function* () {})()); await finishedPromise(rs, { signal }); } assert.rejects(run, { name: 'AbortError' }).then(common.mustCall()); } { const rs = fs.createReadStream('file-does-not-exist'); finished(rs, common.expectsError({ code: 'ENOENT' })); } { const rs = new Readable(); finished(rs, common.mustSucceed()); rs.push(null); rs.emit('close'); // Should not trigger an error rs.resume(); } { const rs = new Readable(); finished(rs, common.mustCall((err) => { assert(err, 'premature close error'); })); rs.emit('close'); // Should trigger error rs.push(null); rs.resume(); } // Test faulty input values and options. { const rs = new Readable({ read() {} }); assert.throws( () => finished(rs, 'foo'), { code: 'ERR_INVALID_ARG_TYPE', message: /callback/ } ); assert.throws( () => finished(rs, 'foo', () => {}), { code: 'ERR_INVALID_ARG_TYPE', message: /options/ } ); assert.throws( () => finished(rs, {}, 'foo'), { code: 'ERR_INVALID_ARG_TYPE', message: /callback/ } ); finished(rs, null, common.mustCall()); rs.push(null); rs.resume(); } // Test that calling returned function removes listeners { const ws = new Writable({ write(data, env, cb) { cb(); } }); const removeListener = finished(ws, common.mustNotCall()); removeListener(); ws.end(); } { const rs = new Readable(); const removeListeners = finished(rs, common.mustNotCall()); removeListeners(); rs.emit('close'); rs.push(null); rs.resume(); } { const streamLike = new EE(); streamLike.readableEnded = true; streamLike.readable = true; assert.throws( () => { finished(streamLike, () => {}); }, { code: 'ERR_INVALID_ARG_TYPE' } ); streamLike.emit('close'); } { const writable = new Writable({ write() {} }); writable.writable = false; writable.destroy(); finished(writable, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); } { const readable = new Readable(); readable.readable = false; readable.destroy(); finished(readable, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); } { const w = new Writable({ write(chunk, encoding, callback) { setImmediate(callback); } }); finished(w, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); w.end('asd'); w.destroy(); } function testClosed(factory) { { // If already destroyed but finished is cancelled in same tick // don't invoke the callback, const s = factory(); s.destroy(); const dispose = finished(s, common.mustNotCall()); dispose(); } { // If already destroyed invoked callback. const s = factory(); s.destroy(); finished(s, common.mustCall()); } { // Don't invoke until destroy has completed. let destroyed = false; const s = factory({ destroy(err, cb) { setImmediate(() => { destroyed = true; cb(); }); } }); s.destroy(); finished(s, common.mustCall(() => { assert.strictEqual(destroyed, true); })); } { // Invoke callback even if close is inhibited. const s = factory({ emitClose: false, destroy(err, cb) { cb(); finished(s, common.mustCall()); } }); s.destroy(); } { // Invoke with deep async. const s = factory({ destroy(err, cb) { setImmediate(() => { cb(); setImmediate(() => { finished(s, common.mustCall()); }); }); } }); s.destroy(); } } testClosed((opts) => new Readable({ ...opts })); testClosed((opts) => new Writable({ write() {}, ...opts })); { const w = new Writable({ write(chunk, encoding, cb) { cb(); }, autoDestroy: false }); w.end('asd'); process.nextTick(() => { finished(w, common.mustCall()); }); } { const w = new Writable({ write(chunk, encoding, cb) { cb(new Error()); }, autoDestroy: false }); w.write('asd'); w.on('error', common.mustCall(() => { finished(w, common.mustCall()); })); } { const r = new Readable({ autoDestroy: false }); r.push(null); r.resume(); r.on('end', common.mustCall(() => { finished(r, common.mustCall()); })); } { const rs = fs.createReadStream(__filename, { autoClose: false }); rs.resume(); rs.on('close', common.mustNotCall()); rs.on('end', common.mustCall(() => { finished(rs, common.mustCall()); })); } { const d = new EE(); d._writableState = {}; d._writableState.finished = true; finished(d, { readable: false, writable: true }, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); d._writableState.errored = true; d.emit('close'); } { const r = new Readable(); finished(r, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); r.push('asd'); r.push(null); r.destroy(); } { const d = new Duplex({ final(cb) { }, // Never close writable side for test purpose read() { this.push(null); } }); d.on('end', common.mustCall()); finished(d, { readable: true, writable: false }, common.mustCall()); d.end(); d.resume(); } { const d = new Duplex({ final(cb) { }, // Never close writable side for test purpose read() { this.push(null); } }); d.on('end', common.mustCall()); d.end(); finished(d, { readable: true, writable: false }, common.mustCall()); d.resume(); } { // Test for compat for e.g. fd-slicer which implements // non standard destroy behavior which might not emit // 'close'. const r = new Readable(); finished(r, common.mustCall()); r.resume(); r.push('asd'); r.destroyed = true; r.push(null); } { // Regression https://github.com/nodejs/node/issues/33130 const response = new PassThrough(); class HelloWorld extends Duplex { constructor(response) { super({ autoDestroy: false }); this.response = response; this.readMore = false; response.once('end', () => { this.push(null); }); response.on('readable', () => { if (this.readMore) { this._read(); } }); } _read() { const { response } = this; this.readMore = true; if (response.readableLength) { this.readMore = false; } let data; while ((data = response.read()) !== null) { this.push(data); } } } const instance = new HelloWorld(response); instance.setEncoding('utf8'); instance.end(); (async () => { await EE.once(instance, 'finish'); setImmediate(() => { response.write('chunk 1'); response.write('chunk 2'); response.write('chunk 3'); response.end(); }); let res = ''; for await (const data of instance) { res += data; } assert.strictEqual(res, 'chunk 1chunk 2chunk 3'); })().then(common.mustCall()); } { const p = new PassThrough(); p.end(); finished(p, common.mustNotCall()); } { const p = new PassThrough(); p.end(); p.on('finish', common.mustCall(() => { finished(p, common.mustNotCall()); })); } { const server = http.createServer(common.mustCall((req, res) => { res.on('close', common.mustCall(() => { finished(res, common.mustCall(() => { server.close(); })); })); res.end(); })) .listen(0, function() { http.request({ method: 'GET', port: this.address().port }).end() .on('response', common.mustCall()); }); } { const server = http.createServer(common.mustCall((req, res) => { req.on('close', common.mustCall(() => { finished(req, common.mustCall(() => { server.close(); })); })); req.destroy(); })).listen(0, function() { http.request({ method: 'GET', port: this.address().port }).end().on('error', common.mustCall()); }); } { const w = new Writable({ write(chunk, encoding, callback) { process.nextTick(callback); } }); w.aborted = false; w.end(); let closed = false; w.on('finish', () => { assert.strictEqual(closed, false); w.emit('aborted'); }); w.on('close', common.mustCall(() => { closed = true; })); finished(w, common.mustCall(() => { assert.strictEqual(closed, true); })); } { const w = new Writable(); const _err = new Error(); w.destroy(_err); assert.strictEqual(w.errored, _err); finished(w, common.mustCall((err) => { assert.strictEqual(_err, err); assert.strictEqual(w.closed, true); finished(w, common.mustCall((err) => { assert.strictEqual(_err, err); })); })); } { const w = new Writable(); w.destroy(); assert.strictEqual(w.errored, null); finished(w, common.mustCall((err) => { assert.strictEqual(w.closed, true); assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); finished(w, common.mustCall((err) => { assert.strictEqual(err.code, 'ERR_STREAM_PREMATURE_CLOSE'); })); })); } { // Legacy Streams do not inherit from Readable or Writable. // We cannot really assume anything about them, so we cannot close them // automatically. const s = new Stream(); finished(s, common.mustNotCall()); } { const server = http.createServer(common.mustCall(function(req, res) { fs.createReadStream(__filename).pipe(res); finished(res, common.mustCall(function(err) { assert.strictEqual(err, undefined); })); })).listen(0, function() { http.request( { method: 'GET', port: this.address().port }, common.mustCall(function(res) { res.resume(); finished(res, common.mustCall(() => { server.close(); })); }) ).end(); }); } { const stream = new Duplex({ write(chunk, enc, cb) { setImmediate(cb); } }); stream.end('foo'); finished(stream, { readable: false }, common.mustCall((err) => { assert(!err); })); }