├── .airtap.yml ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── img.jpg ├── index.js ├── package.json └── test ├── basic.js └── object.js /.airtap.yml: -------------------------------------------------------------------------------- 1 | sauce_connect: true 2 | loopback: airtap.local 3 | browsers: 4 | - name: chrome 5 | version: latest 6 | - name: firefox 7 | version: latest 8 | - name: safari 9 | version: latest 10 | - name: microsoftedge 11 | version: latest 12 | - name: iphone 13 | version: latest 14 | - name: android 15 | version: latest 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .airtap.yml 2 | .travis.yml 3 | img.jpg 4 | test/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | addons: 5 | sauce_connect: true 6 | hosts: 7 | - airtap.local 8 | env: 9 | global: 10 | - secure: LNatP/8w5zyemrTbVV6E4vqfi9BfLwUk3J1eLAEzXvTKu6uVOH/Y6kVkdIHWYCsld4VTGnuJHcq7WAb5xdE0chTKqt6Y0/8uZcgzDVJZgaksC3gKgqXSiajPmpEThfYTygIRGQ45rTHsaGxRMDMQOJxZj3FDS+QOCXYpa6exuYQ= 11 | - secure: AdYifmUGcwW+ZKNk5v0WiVQJl/I7/ZmSRoiLZQ/NDe6Y3uj5ckzearfs1a5QAliTiHafIgQZ/Garo7o3Wty2uXlbVrcS3b8AyL3gtSxKkCbDGZxn9cMMPEYknOifpUIU3aPmfQ8kx/jT2lwk/6Q8EZuS8AkFZhXSnHqQkkym1Bo= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multistream [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/feross/multistream/master.svg 4 | [travis-url]: https://travis-ci.org/feross/multistream 5 | [npm-image]: https://img.shields.io/npm/v/multistream.svg 6 | [npm-url]: https://npmjs.org/package/multistream 7 | [downloads-image]: https://img.shields.io/npm/dm/multistream.svg 8 | [downloads-url]: https://npmjs.org/package/multistream 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | #### A stream that emits multiple other streams one after another (streams3) 13 | 14 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/multistream.svg)](https://saucelabs.com/u/multistream) 15 | 16 | ![cat](https://raw.githubusercontent.com/feross/multistream/master/img.jpg) 17 | 18 | Simple, robust streams3 version of [combined-stream](https://www.npmjs.org/package/combined-stream). Allows you to combine multiple streams into a single stream. When the first stream ends, the next one starts, and so on, until all streams are consumed. 19 | 20 | This module is used by [WebTorrent](http://webtorrent.io), specifically [create-torrent](https://github.com/feross/create-torrent). 21 | 22 | ### install 23 | 24 | ``` 25 | npm install multistream 26 | ``` 27 | 28 | ### usage 29 | 30 | Use `multistream` like this: 31 | 32 | ```js 33 | var MultiStream = require('multistream') 34 | var fs = require('fs') 35 | 36 | var streams = [ 37 | fs.createReadStream(__dirname + '/numbers/1.txt'), 38 | fs.createReadStream(__dirname + '/numbers/2.txt'), 39 | fs.createReadStream(__dirname + '/numbers/3.txt') 40 | ] 41 | 42 | new MultiStream(streams).pipe(process.stdout) // => 123 43 | ``` 44 | 45 | You can also create an object-mode stream with `MultiStream.obj(streams)`. 46 | 47 | To lazily create the streams, wrap them in a function: 48 | 49 | ```js 50 | var streams = [ 51 | fs.createReadStream(__dirname + '/numbers/1.txt'), 52 | function () { // will be executed when the stream is active 53 | return fs.createReadStream(__dirname + '/numbers/2.txt') 54 | }, 55 | function () { // same 56 | return fs.createReadStream(__dirname + '/numbers/3.txt') 57 | } 58 | ] 59 | 60 | new MultiStream(streams).pipe(process.stdout) // => 123 61 | ``` 62 | 63 | Alternatively, streams may be created by an asynchronous "factory" function: 64 | 65 | ```js 66 | var count = 0 67 | function factory (cb) { 68 | if (count > 3) return cb(null, null) 69 | count++ 70 | setTimeout(function () { 71 | cb(null, fs.createReadStream(__dirname + '/numbers/' + count + '.txt')) 72 | }, 100) 73 | } 74 | 75 | new MultiStream(factory).pipe(process.stdout) // => 123 76 | ``` 77 | 78 | ### contributors 79 | 80 | - [Feross Aboukhadijeh](http://feross.org) 81 | - [Mathias Buus](https://github.com/mafintosh/) 82 | - [Yuri Astrakhan](https://github.com/nyurik/) 83 | 84 | ### license 85 | 86 | MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). 87 | -------------------------------------------------------------------------------- /img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/multistream/aef3ef79dcb0514d1f20f3434597892e3444fa5f/img.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! multistream. MIT License. Feross Aboukhadijeh */ 2 | const stream = require('readable-stream') 3 | const once = require('once') 4 | 5 | function toStreams2Obj (s) { 6 | return toStreams2(s, { objectMode: true, highWaterMark: 16 }) 7 | } 8 | 9 | function toStreams2Buf (s) { 10 | return toStreams2(s) 11 | } 12 | 13 | function toStreams2 (s, opts) { 14 | if (!s || typeof s === 'function' || s._readableState) return s 15 | 16 | const wrap = new stream.Readable(opts).wrap(s) 17 | if (s.destroy) { 18 | wrap.destroy = s.destroy.bind(s) 19 | } 20 | return wrap 21 | } 22 | 23 | class MultiStream extends stream.Readable { 24 | constructor (streams, opts) { 25 | super({ ...opts, autoDestroy: true }) 26 | 27 | this._drained = false 28 | this._forwarding = false 29 | this._current = null 30 | this._toStreams2 = (opts && opts.objectMode) ? toStreams2Obj : toStreams2Buf 31 | 32 | if (typeof streams === 'function') { 33 | this._queue = streams 34 | } else { 35 | this._queue = streams.map(this._toStreams2) 36 | this._queue.forEach(stream => { 37 | if (typeof stream !== 'function') this._attachErrorListener(stream) 38 | }) 39 | } 40 | 41 | this._next() 42 | } 43 | 44 | _read () { 45 | this._drained = true 46 | this._forward() 47 | } 48 | 49 | _forward () { 50 | if (this._forwarding || !this._drained || !this._current) return 51 | this._forwarding = true 52 | 53 | let chunk 54 | while (this._drained && (chunk = this._current.read()) !== null) { 55 | this._drained = this.push(chunk) 56 | } 57 | 58 | this._forwarding = false 59 | } 60 | 61 | _destroy (err, cb) { 62 | let streams = [] 63 | if (this._current) streams.push(this._current) 64 | if (typeof this._queue !== 'function') streams = streams.concat(this._queue) 65 | 66 | if (streams.length === 0) { 67 | cb(err) 68 | } else { 69 | let counter = streams.length 70 | let er = err 71 | streams.forEach(stream => { 72 | destroy(stream, err, err => { 73 | er = er || err 74 | if (--counter === 0) { 75 | cb(er) 76 | } 77 | }) 78 | }) 79 | } 80 | } 81 | 82 | _next () { 83 | this._current = null 84 | 85 | if (typeof this._queue === 'function') { 86 | this._queue((err, stream) => { 87 | if (err) return this.destroy(err) 88 | stream = this._toStreams2(stream) 89 | this._attachErrorListener(stream) 90 | this._gotNextStream(stream) 91 | }) 92 | } else { 93 | let stream = this._queue.shift() 94 | if (typeof stream === 'function') { 95 | stream = this._toStreams2(stream()) 96 | this._attachErrorListener(stream) 97 | } 98 | this._gotNextStream(stream) 99 | } 100 | } 101 | 102 | _gotNextStream (stream) { 103 | if (!stream) { 104 | this.push(null) 105 | return 106 | } 107 | 108 | this._current = stream 109 | this._forward() 110 | 111 | const onReadable = () => { 112 | this._forward() 113 | } 114 | 115 | const onClose = () => { 116 | if (!stream._readableState.ended && !stream.destroyed) { 117 | const err = new Error('ERR_STREAM_PREMATURE_CLOSE') 118 | err.code = 'ERR_STREAM_PREMATURE_CLOSE' 119 | this.destroy(err) 120 | } 121 | } 122 | 123 | const onEnd = () => { 124 | this._current = null 125 | stream.removeListener('readable', onReadable) 126 | stream.removeListener('end', onEnd) 127 | stream.removeListener('close', onClose) 128 | stream.destroy() 129 | this._next() 130 | } 131 | 132 | stream.on('readable', onReadable) 133 | stream.once('end', onEnd) 134 | stream.once('close', onClose) 135 | } 136 | 137 | _attachErrorListener (stream) { 138 | if (!stream) return 139 | 140 | const onError = (err) => { 141 | stream.removeListener('error', onError) 142 | this.destroy(err) 143 | } 144 | 145 | stream.once('error', onError) 146 | } 147 | } 148 | 149 | MultiStream.obj = streams => ( 150 | new MultiStream(streams, { objectMode: true, highWaterMark: 16 }) 151 | ) 152 | 153 | module.exports = MultiStream 154 | 155 | // Normalize stream destroy w/ callback. 156 | function destroy (stream, err, cb) { 157 | if (!stream.destroy || stream.destroyed) { 158 | cb(err) 159 | } else { 160 | const callback = once(er => cb(er || err)) 161 | stream 162 | .on('error', callback) 163 | .on('close', () => callback()) 164 | .destroy(err, callback) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multistream", 3 | "description": "A stream that emits multiple other streams one after another (streams3)", 4 | "version": "4.1.0", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/multistream/issues" 12 | }, 13 | "dependencies": { 14 | "once": "^1.4.0", 15 | "readable-stream": "^3.6.0" 16 | }, 17 | "devDependencies": { 18 | "airtap": "^3.0.0", 19 | "array-to-stream": "^1.0.2", 20 | "simple-concat": "^1.0.1", 21 | "standard": "*", 22 | "string-to-stream": "^3.0.1", 23 | "tape": "^5.0.1", 24 | "through": "^2.3.8" 25 | }, 26 | "homepage": "https://github.com/feross/multistream", 27 | "keywords": [ 28 | "combine streams", 29 | "join streams", 30 | "concat streams", 31 | "multiple streams", 32 | "combine", 33 | "join", 34 | "concat", 35 | "multiple", 36 | "file stream", 37 | "append", 38 | "append streams", 39 | "combiner", 40 | "joiner" 41 | ], 42 | "license": "MIT", 43 | "main": "index.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git://github.com/feross/multistream.git" 47 | }, 48 | "scripts": { 49 | "test": "standard && npm run test-node && npm run test-browser", 50 | "test-browser": "airtap -- test/*.js", 51 | "test-browser-local": "airtap --local -- test/*.js", 52 | "test-node": "tape test/*.js" 53 | }, 54 | "funding": [ 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/feross" 58 | }, 59 | { 60 | "type": "patreon", 61 | "url": "https://www.patreon.com/feross" 62 | }, 63 | { 64 | "type": "consulting", 65 | "url": "https://feross.org/support" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const concat = require('simple-concat') 2 | const MultiStream = require('../') 3 | const str = require('string-to-stream') 4 | const test = require('tape') 5 | const through = require('through') 6 | 7 | test('combine streams', function (t) { 8 | const streams = [ 9 | str('1'), 10 | str('2'), 11 | str('3') 12 | ] 13 | 14 | const stream = new MultiStream(streams) 15 | .on('error', function (err) { 16 | t.fail(err) 17 | }) 18 | 19 | concat(stream, function (err, data) { 20 | t.error(err) 21 | t.equal(data.toString(), '123') 22 | t.end() 23 | }) 24 | }) 25 | 26 | test('combine streams (classic)', function (t) { 27 | const streams = [ 28 | through(), 29 | through(), 30 | through() 31 | ] 32 | 33 | const stream = new MultiStream(streams) 34 | .on('error', function (err) { 35 | t.fail(err) 36 | }) 37 | 38 | concat(stream, function (err, data) { 39 | t.error(err) 40 | t.equal(data.toString(), '123') 41 | t.end() 42 | }) 43 | 44 | streams[0].end('1') 45 | streams[1].end('2') 46 | streams[2].end('3') 47 | }) 48 | 49 | test('lazy stream creation', function (t) { 50 | const streams = [ 51 | str('1'), 52 | function () { 53 | return str('2') 54 | }, 55 | function () { 56 | return str('3') 57 | } 58 | ] 59 | 60 | const stream = new MultiStream(streams) 61 | .on('error', function (err) { 62 | t.fail(err) 63 | }) 64 | 65 | concat(stream, function (err, data) { 66 | t.error(err) 67 | t.equal(data.toString(), '123') 68 | t.end() 69 | }) 70 | }) 71 | 72 | test('lazy stream via factory', function (t) { 73 | let count = 0 74 | function factory (cb) { 75 | if (count > 2) return cb(null, null) 76 | count++ 77 | setTimeout(function () { 78 | cb(null, str(count.toString())) 79 | }, 0) 80 | } 81 | 82 | const stream = new MultiStream(factory) 83 | .on('error', function (err) { 84 | t.fail(err) 85 | }) 86 | 87 | concat(stream, function (err, data) { 88 | t.error(err) 89 | t.equal(data.toString(), '123') 90 | t.end() 91 | }) 92 | }) 93 | 94 | test('lazy stream via factory (factory returns error)', function (t) { 95 | t.plan(2) 96 | let count = 0 97 | function factory (cb) { 98 | if (count > 2) return cb(new Error('factory error')) 99 | count++ 100 | setTimeout(function () { 101 | cb(null, str(count.toString())) 102 | }, 0) 103 | } 104 | 105 | new MultiStream(factory) 106 | .on('error', function (err) { 107 | t.pass('got error', err) 108 | }) 109 | .on('close', function () { 110 | t.pass('got close') 111 | }) 112 | .resume() 113 | }) 114 | 115 | test('lazy stream via factory (classic)', function (t) { 116 | let count = 0 117 | function factory (cb) { 118 | if (count > 2) return cb(null, null) 119 | count++ 120 | const s = through() 121 | process.nextTick(function () { 122 | s.write(count.toString()) 123 | s.end() 124 | }) 125 | cb(null, s) 126 | } 127 | 128 | const stream = new MultiStream(factory) 129 | .on('error', function (err) { 130 | t.fail(err) 131 | }) 132 | 133 | concat(stream, function (err, data) { 134 | t.error(err) 135 | t.equal(data.toString(), '123') 136 | t.end() 137 | }) 138 | }) 139 | 140 | test('throw immediate error', function (t) { 141 | t.plan(1) 142 | 143 | const streams = [ 144 | str('1'), 145 | through() // will emit 'error' 146 | ] 147 | 148 | new MultiStream(streams).on('error', function (err) { 149 | t.ok(err instanceof Error, 'got expected error') 150 | }) 151 | 152 | streams[1].emit('error', new Error('immediate error!')) 153 | }) 154 | -------------------------------------------------------------------------------- /test/object.js: -------------------------------------------------------------------------------- 1 | const MultiStream = require('../') 2 | const ary = require('array-to-stream') 3 | const test = require('tape') 4 | 5 | test('combine object streams', function (t) { 6 | const objects = [true, { x: 'b' }, 'c', 'd', 'e', 'f'] 7 | 8 | const streams = [ 9 | ary(objects.slice(0, 2)), 10 | ary(objects.slice(2, 3)), 11 | ary(objects.slice(3)) 12 | ] 13 | 14 | const received = [] 15 | MultiStream.obj(streams) 16 | .on('error', function (err) { 17 | t.fail(err) 18 | }) 19 | .on('data', function (object) { 20 | received.push(object) 21 | }) 22 | .on('end', function () { 23 | t.same(objects, received) 24 | t.end() 25 | }) 26 | }) 27 | --------------------------------------------------------------------------------