├── .npmignore ├── .gitignore ├── .travis.yml ├── SECURITY.md ├── History.md ├── package.json ├── lib └── merge-promise.js ├── index.js ├── Readme.md └── test └── multipipe.js /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sw* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | arch: 2 | - amd64 3 | - ppc64le 4 | language: node_js 5 | node_js: 6 | - 12 7 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.1.1 / 2014-06-01 3 | ================== 4 | 5 | * update duplexer2 dep 6 | 7 | 0.1.0 / 2014-05-24 8 | ================== 9 | 10 | * add optional callback 11 | 12 | 0.0.2 / 2014-02-20 13 | ================== 14 | 15 | * fix infinite loop 16 | 17 | 0.0.1 / 2014-01-15 18 | ================== 19 | 20 | * fix error bubbling 21 | 22 | 0.0.0 / 2014-01-13 23 | ================== 24 | 25 | * initial release 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multipipe", 3 | "version": "4.0.0", 4 | "description": "pipe streams with centralized error handling", 5 | "license": "MIT", 6 | "repository": "juliangruber/multipipe", 7 | "dependencies": { 8 | "duplexer2": "^0.1.2", 9 | "object-assign": "^4.1.0" 10 | }, 11 | "devDependencies": { 12 | "mocha": "^7.0.1", 13 | "prettier-standard": "^16.0.0", 14 | "standard": "^14.0.2", 15 | "through2": "^3.0.0" 16 | }, 17 | "scripts": { 18 | "test": "prettier-standard '**/*.js' 'test/*.js' && standard && mocha --reporter spec --timeout 300" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/merge-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Adapted from 4 | // https://github.com/sindresorhus/execa/blob/120230cade59099214905ac2a9136e406c0b6f3a/lib/promise.js 5 | 6 | const descriptors = ['then', 'catch', 'finally'].map(property => [ 7 | property, 8 | Reflect.getOwnPropertyDescriptor(Promise.prototype, property) 9 | ]) 10 | 11 | module.exports = (stream, createPromise) => { 12 | for (const [property, descriptor] of descriptors) { 13 | const value = (...args) => 14 | Reflect.apply(descriptor.value, createPromise(), args) 15 | Reflect.defineProperty(stream, property, { ...descriptor, value }) 16 | } 17 | return stream 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | const duplexer = require('duplexer2') 6 | const { PassThrough, Readable } = require('stream') 7 | const mergePromise = require('./lib/merge-promise') 8 | 9 | /** 10 | * Duplexer options. 11 | */ 12 | 13 | const defaultOpts = { 14 | bubbleErrors: true, 15 | objectMode: true 16 | } 17 | 18 | /** 19 | * Pipe. 20 | * 21 | * @param streams Array[Stream,...] 22 | * @param opts [Object] 23 | * @param cb [Function] 24 | * @return {Stream} 25 | * @api public 26 | */ 27 | 28 | const pipe = (...streams) => { 29 | let opts, cb 30 | 31 | if (typeof streams[streams.length - 1] === 'function') { 32 | cb = streams.pop() 33 | } 34 | if ( 35 | typeof streams[streams.length - 1] === 'object' && 36 | !Array.isArray(streams[streams.length - 1]) && 37 | typeof streams[streams.length - 1].pipe !== 'function' 38 | ) { 39 | opts = streams.pop() 40 | } 41 | if (Array.isArray(streams[0])) { 42 | streams = streams[0] 43 | } 44 | 45 | let first = streams[0] 46 | let last = streams[streams.length - 1] 47 | let ret 48 | opts = Object.assign({}, defaultOpts, opts) 49 | 50 | if (!first) { 51 | ret = first = last = new PassThrough(opts) 52 | process.nextTick(() => ret.end()) 53 | } else if (first.writable && last.readable) { 54 | ret = duplexer(opts, first, last) 55 | } else if (streams.length === 1) { 56 | ret = new Readable(opts).wrap(streams[0]) 57 | } else if (first.writable) { 58 | ret = first 59 | } else if (last.readable) { 60 | ret = last 61 | } else { 62 | ret = new PassThrough(opts) 63 | } 64 | 65 | for (const [i, stream] of streams.entries()) { 66 | const next = streams[i + 1] 67 | if (next) stream.pipe(next) 68 | if (stream !== ret) stream.on('error', err => ret.emit('error', err)) 69 | } 70 | 71 | if (cb) { 72 | let ended = false 73 | const end = err => { 74 | if (ended) return 75 | ended = true 76 | cb(err) 77 | } 78 | ret.on('error', end) 79 | last.on('finish', () => end()) 80 | last.on('close', () => end()) 81 | } 82 | 83 | const createPromise = () => 84 | new Promise((resolve, reject) => { 85 | ret.on('error', reject) 86 | last.on('finish', resolve) 87 | last.on('close', resolve) 88 | }) 89 | 90 | return mergePromise(ret, createPromise) 91 | } 92 | 93 | /** 94 | * Expose `pipe`. 95 | */ 96 | 97 | module.exports = pipe 98 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # multipipe 2 | 3 | A better `Stream#pipe` that creates duplex streams and lets you handle errors in one place. With promise support! 4 | 5 | [![build status](https://travis-ci.org/juliangruber/multipipe.svg?branch=master)](http://travis-ci.org/juliangruber/multipipe) 6 | [![downloads](https://img.shields.io/npm/dm/multipipe.svg)](https://www.npmjs.org/package/multipipe) [![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/multipipe.svg)](https://greenkeeper.io/) 7 | 8 | ## Example 9 | 10 | ```js 11 | const pipe = require('multipipe') 12 | 13 | // pipe streams 14 | const stream = pipe(streamA, streamB, streamC) 15 | 16 | // centralized error handling 17 | stream.on('error', fn) 18 | 19 | // creates a new stream 20 | source.pipe(stream).pipe(dest) 21 | 22 | // optional callback on finish or error 23 | pipe(streamA, streamB, streamC, err => { 24 | // ... 25 | }) 26 | 27 | // pass options 28 | pipe(streamA, streamB, streamC, { 29 | objectMode: false 30 | }) 31 | 32 | // await finish 33 | await pipe(streamA, streamB, streamC) 34 | ``` 35 | 36 | ## Duplex streams 37 | 38 | Write to the pipe and you'll really write to the first stream, read from the pipe and you'll read from the last stream. 39 | 40 | ```js 41 | const stream = pipe(a, b, c) 42 | 43 | source 44 | .pipe(stream) 45 | .pipe(destination) 46 | ``` 47 | 48 | In this example the flow of data is: 49 | 50 | * source -> 51 | * a -> 52 | * b -> 53 | * c -> 54 | * destination 55 | 56 | ## Error handling 57 | 58 | Each `pipe` forwards the errors the streams it wraps emit, so you have one central place to handle errors: 59 | 60 | ```js 61 | const stream = pipe(a, b, c) 62 | 63 | stream.on('error', err => { 64 | // called three times 65 | }) 66 | 67 | a.emit('error', new Error) 68 | b.emit('error', new Error) 69 | c.emit('error', new Error) 70 | ``` 71 | 72 | ## API 73 | 74 | ### pipe(stream...[, opts][, cb]) 75 | 76 | Pass a variable number of streams and each will be piped to the next one. 77 | 78 | A stream will be returned that wraps passed in streams in a way that errors will be forwarded and you can write to and/or read from it. 79 | 80 | The returned stream is also a `Promise` that will resolve on finish and reject on error. 81 | 82 | Pass an object as the second to last or last argument to pass as `options` to the underlying stream constructors. 83 | 84 | Pass a function as last argument to be called on `error` or `finish` of the last stream. 85 | 86 | ### pipe(streams[, cb]) 87 | 88 | You can also pass an `Array` of streams if that is more convenient. 89 | 90 | ## Installation 91 | 92 | ```bash 93 | $ npm install multipipe 94 | ``` 95 | 96 | ## License 97 | 98 | The MIT License (MIT) 99 | 100 | Copyright (c) 2014 Segment.io Inc. <friends@segment.io> 101 | 102 | Copyright (c) 2014 Julian Gruber <julian@juliangruber.com> 103 | 104 | Permission is hereby granted, free of charge, to any person obtaining a copy 105 | of this software and associated documentation files (the "Software"), to deal 106 | in the Software without restriction, including without limitation the rights 107 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 108 | copies of the Software, and to permit persons to whom the Software is 109 | furnished to do so, subject to the following conditions: 110 | 111 | The above copyright notice and this permission notice shall be included in 112 | all copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 115 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 116 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 117 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 118 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 119 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 120 | THE SOFTWARE. 121 | -------------------------------------------------------------------------------- /test/multipipe.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const assert = require('assert') 4 | const pipe = require('..') 5 | const Stream = require('stream') 6 | const through = require('through2') 7 | 8 | const Readable = () => { 9 | const readable = new Stream.Readable({ objectMode: true }) 10 | readable._read = function () { 11 | this.push('a') 12 | this.push(null) 13 | } 14 | return readable 15 | } 16 | 17 | const Transform = () => { 18 | const transform = new Stream.Transform({ objectMode: true }) 19 | transform._transform = (chunk, _, done) => { 20 | done(null, chunk.toUpperCase()) 21 | } 22 | return transform 23 | } 24 | 25 | const Writable = cb => { 26 | const writable = new Stream.Writable({ objectMode: true }) 27 | writable._write = (chunk, _, done) => { 28 | assert.strictEqual(chunk, 'A') 29 | done() 30 | cb && cb() 31 | } 32 | return writable 33 | } 34 | 35 | describe('pipe()', () => { 36 | it('should return a stream', done => { 37 | assert(pipe(done)) 38 | }) 39 | it('should accept options', () => { 40 | assert.strictEqual( 41 | pipe({ objectMode: false })._readableState.objectMode, 42 | false 43 | ) 44 | }) 45 | }) 46 | 47 | describe('pipe(a)', () => { 48 | it('should pass through to a', done => { 49 | Readable() 50 | .pipe(pipe(Transform())) 51 | .pipe(Writable(done)) 52 | }) 53 | it('should accept options', () => { 54 | const readable = Readable({ objectMode: true }) 55 | assert.strictEqual( 56 | pipe(readable, { objectMode: false })._readableState.objectMode, 57 | false 58 | ) 59 | }) 60 | }) 61 | 62 | describe('pipe(a, b, c)', () => { 63 | it('should pipe internally', done => { 64 | pipe(Readable(), Transform(), Writable(done)) 65 | }) 66 | 67 | it('should be writable', done => { 68 | const stream = pipe(Transform(), Writable(done)) 69 | assert(stream.writable) 70 | Readable().pipe(stream) 71 | }) 72 | 73 | it('should be readable', done => { 74 | const stream = pipe(Readable(), Transform()) 75 | assert(stream.readable) 76 | stream.pipe(Writable(done)) 77 | }) 78 | 79 | it('should be readable and writable', done => { 80 | const stream = pipe(Transform(), Transform()) 81 | assert(stream.readable) 82 | assert(stream.writable) 83 | Readable() 84 | .pipe(stream) 85 | .pipe(Writable(done)) 86 | }) 87 | 88 | describe('errors', () => { 89 | it('should reemit', done => { 90 | const a = Transform() 91 | const b = Transform() 92 | const c = Transform() 93 | const stream = pipe(a, b, c) 94 | const err = new Error() 95 | let i = 0 96 | 97 | stream.on('error', _err => { 98 | i++ 99 | assert.strictEqual(_err, err) 100 | assert(i <= 3) 101 | if (i === 3) done() 102 | }) 103 | 104 | a.emit('error', err) 105 | b.emit('error', err) 106 | c.emit('error', err) 107 | }) 108 | 109 | it('should not reemit endlessly', done => { 110 | const a = Transform() 111 | const b = Transform() 112 | const c = Transform() 113 | c.readable = false 114 | const stream = pipe(a, b, c) 115 | const err = new Error() 116 | let i = 0 117 | 118 | stream.on('error', function (_err) { 119 | i++ 120 | assert.strictEqual(_err, err) 121 | assert(i <= 3) 122 | if (i === 3) done() 123 | }) 124 | 125 | a.emit('error', err) 126 | b.emit('error', err) 127 | c.emit('error', err) 128 | }) 129 | }) 130 | it('should accept options', () => { 131 | const a = Readable() 132 | const b = Transform() 133 | const c = Writable() 134 | assert.strictEqual( 135 | pipe(a, b, c, { objectMode: false })._readableState.objectMode, 136 | false 137 | ) 138 | }) 139 | }) 140 | 141 | describe('pipe(a, b, c, fn)', () => { 142 | it('should call on finish', done => { 143 | let finished = false 144 | const a = Readable() 145 | const b = Transform() 146 | const c = Writable(function () { 147 | finished = true 148 | }) 149 | 150 | pipe(a, b, c, err => { 151 | assert(!err) 152 | assert(finished) 153 | done() 154 | }) 155 | }) 156 | 157 | it('should call with error once', done => { 158 | const a = Readable() 159 | const b = Transform() 160 | const c = Writable() 161 | const err = new Error() 162 | 163 | pipe(a, b, c, err => { 164 | assert(err) 165 | done() 166 | }) 167 | 168 | a.emit('error', err) 169 | b.emit('error', err) 170 | c.emit('error', err) 171 | }) 172 | 173 | it('should call on destroy', done => { 174 | const a = Readable() 175 | const b = Transform() 176 | const c = through() 177 | 178 | pipe(a, b, c, err => { 179 | assert(!err) 180 | done() 181 | }) 182 | 183 | c.destroy() 184 | }) 185 | 186 | it('should call on destroy with error', done => { 187 | const a = Readable() 188 | const b = Transform() 189 | const c = through() 190 | const err = new Error() 191 | 192 | pipe(a, b, c, _err => { 193 | assert.strictEqual(_err, err) 194 | done() 195 | }) 196 | 197 | c.destroy(err) 198 | }) 199 | 200 | it('should accept options', done => { 201 | const a = Readable() 202 | const b = Transform() 203 | const c = Writable() 204 | assert.strictEqual( 205 | pipe(a, b, c, { objectMode: false }, done)._readableState.objectMode, 206 | false 207 | ) 208 | }) 209 | 210 | it('should ignore parameters on non error events', done => { 211 | const a = Readable() 212 | const b = Transform() 213 | const c = Writable() 214 | pipe(a, b, c, done) 215 | c.emit('finish', true) 216 | }) 217 | }) 218 | 219 | describe('pipe([a, b, c], fn)', () => { 220 | it('should call on finish', done => { 221 | let finished = false 222 | const a = Readable() 223 | const b = Transform() 224 | const c = Writable(function () { 225 | finished = true 226 | }) 227 | 228 | pipe([a, b, c], err => { 229 | assert(!err) 230 | assert(finished) 231 | done() 232 | }) 233 | }) 234 | }) 235 | 236 | describe('await pipe(a, b, c)', () => { 237 | it('should resolve on finish', done => { 238 | let finished = false 239 | const a = Readable() 240 | const b = Transform() 241 | const c = Writable(function () { 242 | finished = true 243 | }) 244 | 245 | pipe(a, b, c).then(() => { 246 | assert(finished) 247 | done() 248 | }) 249 | }) 250 | 251 | it('should reject on error', done => { 252 | const a = Readable() 253 | const b = Transform() 254 | const c = Writable() 255 | const err = new Error() 256 | 257 | pipe(a, b, c).catch(_err => { 258 | assert.strictEqual(_err, err) 259 | done() 260 | }) 261 | 262 | b.emit('error', err) 263 | }) 264 | }) 265 | --------------------------------------------------------------------------------