├── .gitignore ├── .travis.yml ├── package.json ├── LICENSE ├── README.md ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | - '6' 6 | - '4' 7 | arch: 8 | - amd64 9 | - ppc64le 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "end-of-stream", 3 | "version": "1.4.5", 4 | "description": "Call a callback when a readable/writable/duplex stream has completed or failed.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/mafintosh/end-of-stream.git" 8 | }, 9 | "dependencies": { 10 | "once": "^1.4.0" 11 | }, 12 | "scripts": { 13 | "test": "node test.js" 14 | }, 15 | "files": [ 16 | "index.js" 17 | ], 18 | "keywords": [ 19 | "stream", 20 | "streams", 21 | "callback", 22 | "finish", 23 | "close", 24 | "end", 25 | "wait" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/mafintosh/end-of-stream/issues" 29 | }, 30 | "homepage": "https://github.com/mafintosh/end-of-stream", 31 | "main": "index.js", 32 | "author": "Mathias Buus ", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "tape": "^4.11.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # end-of-stream 2 | 3 | A node module that calls a callback when a readable/writable/duplex stream has completed or failed. 4 | 5 | npm install end-of-stream 6 | 7 | [![Build status](https://travis-ci.org/mafintosh/end-of-stream.svg?branch=master)](https://travis-ci.org/mafintosh/end-of-stream) 8 | 9 | ## Usage 10 | 11 | Simply pass a stream and a callback to the `eos`. 12 | Both legacy streams, streams2 and stream3 are supported. 13 | 14 | ``` js 15 | var eos = require('end-of-stream'); 16 | 17 | eos(readableStream, function(err) { 18 | // this will be set to the stream instance 19 | if (err) return console.log('stream had an error or closed early'); 20 | console.log('stream has ended', this === readableStream); 21 | }); 22 | 23 | eos(writableStream, function(err) { 24 | if (err) return console.log('stream had an error or closed early'); 25 | console.log('stream has finished', this === writableStream); 26 | }); 27 | 28 | eos(duplexStream, function(err) { 29 | if (err) return console.log('stream had an error or closed early'); 30 | console.log('stream has ended and finished', this === duplexStream); 31 | }); 32 | 33 | eos(duplexStream, {readable:false}, function(err) { 34 | if (err) return console.log('stream had an error or closed early'); 35 | console.log('stream has finished but might still be readable'); 36 | }); 37 | 38 | eos(duplexStream, {writable:false}, function(err) { 39 | if (err) return console.log('stream had an error or closed early'); 40 | console.log('stream has ended but might still be writable'); 41 | }); 42 | 43 | eos(readableStream, {error:false}, function(err) { 44 | // do not treat emit('error', err) as a end-of-stream 45 | }); 46 | ``` 47 | 48 | ## License 49 | 50 | MIT 51 | 52 | ## Related 53 | 54 | `end-of-stream` is part of the [mississippi stream utility collection](https://github.com/maxogden/mississippi) which includes more useful stream modules similar to this one. 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var once = require('once'); 2 | 3 | var noop = function() {}; 4 | 5 | var qnt = global.Bare ? queueMicrotask : process.nextTick.bind(process); 6 | 7 | var isRequest = function(stream) { 8 | return stream.setHeader && typeof stream.abort === 'function'; 9 | }; 10 | 11 | var isChildProcess = function(stream) { 12 | return stream.stdio && Array.isArray(stream.stdio) && stream.stdio.length === 3 13 | }; 14 | 15 | var eos = function(stream, opts, callback) { 16 | if (typeof opts === 'function') return eos(stream, null, opts); 17 | if (!opts) opts = {}; 18 | 19 | callback = once(callback || noop); 20 | 21 | var ws = stream._writableState; 22 | var rs = stream._readableState; 23 | var readable = opts.readable || (opts.readable !== false && stream.readable); 24 | var writable = opts.writable || (opts.writable !== false && stream.writable); 25 | var cancelled = false; 26 | 27 | var onlegacyfinish = function() { 28 | if (!stream.writable) onfinish(); 29 | }; 30 | 31 | var onfinish = function() { 32 | writable = false; 33 | if (!readable) callback.call(stream); 34 | }; 35 | 36 | var onend = function() { 37 | readable = false; 38 | if (!writable) callback.call(stream); 39 | }; 40 | 41 | var onexit = function(exitCode) { 42 | callback.call(stream, exitCode ? new Error('exited with error code: ' + exitCode) : null); 43 | }; 44 | 45 | var onerror = function(err) { 46 | callback.call(stream, err); 47 | }; 48 | 49 | var onclose = function() { 50 | qnt(onclosenexttick); 51 | }; 52 | 53 | var onclosenexttick = function() { 54 | if (cancelled) return; 55 | if (readable && !(rs && (rs.ended && !rs.destroyed))) return callback.call(stream, new Error('premature close')); 56 | if (writable && !(ws && (ws.ended && !ws.destroyed))) return callback.call(stream, new Error('premature close')); 57 | }; 58 | 59 | var onrequest = function() { 60 | stream.req.on('finish', onfinish); 61 | }; 62 | 63 | if (isRequest(stream)) { 64 | stream.on('complete', onfinish); 65 | stream.on('abort', onclose); 66 | if (stream.req) onrequest(); 67 | else stream.on('request', onrequest); 68 | } else if (writable && !ws) { // legacy streams 69 | stream.on('end', onlegacyfinish); 70 | stream.on('close', onlegacyfinish); 71 | } 72 | 73 | if (isChildProcess(stream)) stream.on('exit', onexit); 74 | 75 | stream.on('end', onend); 76 | stream.on('finish', onfinish); 77 | if (opts.error !== false) stream.on('error', onerror); 78 | stream.on('close', onclose); 79 | 80 | return function() { 81 | cancelled = true; 82 | stream.removeListener('complete', onfinish); 83 | stream.removeListener('abort', onclose); 84 | stream.removeListener('request', onrequest); 85 | if (stream.req) stream.req.removeListener('finish', onfinish); 86 | stream.removeListener('end', onlegacyfinish); 87 | stream.removeListener('close', onlegacyfinish); 88 | stream.removeListener('finish', onfinish); 89 | stream.removeListener('exit', onexit); 90 | stream.removeListener('end', onend); 91 | stream.removeListener('error', onerror); 92 | stream.removeListener('close', onclose); 93 | }; 94 | }; 95 | 96 | module.exports = eos; 97 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var eos = require('./index') 3 | 4 | var fs = require('fs') 5 | var cp = require('child_process') 6 | var net = require('net') 7 | var http = require('http') 8 | var stream = require('stream') 9 | 10 | tape('fs writestream destory', function (t) { 11 | var ws = fs.createWriteStream('/dev/null'); 12 | 13 | eos(ws, function(err) { 14 | t.ok(!!err) 15 | t.ok(this === ws) 16 | t.end() 17 | }) 18 | 19 | ws.destroy(); 20 | }) 21 | 22 | tape('fs readstream destroy', function (t) { 23 | var rs1 = fs.createReadStream('/dev/urandom'); 24 | 25 | eos(rs1, function(err) { 26 | t.ok(!!err) 27 | t.ok(this === rs1) 28 | t.end() 29 | }) 30 | 31 | rs1.destroy() 32 | }) 33 | 34 | tape('fs readstream pipe', function (t) { 35 | var rs2 = fs.createReadStream(__filename) 36 | 37 | eos(rs2, function(err) { 38 | t.ifError(err) 39 | t.ok(this === rs2) 40 | t.end() 41 | }) 42 | 43 | rs2.pipe(fs.createWriteStream('/dev/null')) 44 | }) 45 | 46 | tape('fs readstream cancel', function (t) { 47 | var rs3 = fs.createReadStream(__filename) 48 | 49 | eos(rs3, function(err) { 50 | t.fail('should not enter') 51 | })() 52 | 53 | rs3.pipe(fs.createWriteStream('/dev/null')) 54 | rs3.on('end', function () { 55 | t.end() 56 | }) 57 | }) 58 | 59 | tape('exec', function (t) { 60 | var exec = cp.exec('echo hello world') 61 | 62 | eos(exec, function (err) { 63 | t.ifError(err) 64 | t.ok(this === exec) 65 | t.end() 66 | }) 67 | }) 68 | 69 | tape('spawn', function (t) { 70 | var spawn = cp.spawn('echo', ['hello world']); 71 | eos(spawn, function (err) { 72 | t.ifError(err) 73 | t.ok(this === spawn) 74 | t.end() 75 | }) 76 | }) 77 | 78 | tape('tcp socket', function (t) { 79 | t.plan(5) 80 | 81 | var socket = net.connect(50000) 82 | 83 | eos(socket, function(err) { 84 | t.ok(!!err) 85 | t.ok(this === socket) 86 | }) 87 | 88 | var server = net.createServer(function (socket) { 89 | eos(socket, function(err) { 90 | t.ok(!!err) 91 | t.ok(this === socket) 92 | }) 93 | socket.destroy() 94 | }).listen(30000, function () { 95 | var socket = net.connect(30000) 96 | eos(socket, function() { 97 | t.ok(this === socket) 98 | server.close() 99 | }) 100 | }) 101 | }) 102 | 103 | tape('http', function (t) { 104 | t.plan(2) 105 | 106 | var server2 = http.createServer(function(req, res) { 107 | eos(res, function(err) { 108 | t.ifError(err) 109 | }) 110 | res.end() 111 | }).listen(function() { 112 | var port = server2.address().port 113 | http.get('http://localhost:' + port, function(res) { 114 | eos(res, function(err) { 115 | t.ifError(err) 116 | server2.close() 117 | }) 118 | res.resume() 119 | }) 120 | }) 121 | }) 122 | 123 | tape('end() and emit(close)', function (t) { 124 | if (!stream.Writable) return t.end() 125 | var ws = new stream.Writable() 126 | 127 | ws._write = function (data, enc, cb) { 128 | process.nextTick(cb) 129 | } 130 | 131 | eos(ws, function (err) { 132 | t.error(err, 'no error') 133 | t.end() 134 | }) 135 | 136 | ws.write('hi') 137 | ws.end() 138 | ws.emit('close') 139 | }) 140 | --------------------------------------------------------------------------------