├── index.coffee ├── index.js ├── .gitignore ├── testcase-request.coffee ├── test.coffee ├── gulpfile.js ├── package.json ├── README.md └── lib ├── combined-stream2.coffee └── combined-stream2.js /index.coffee: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/combined-stream2"); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/combined-stream2"); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://git-scm.com/docs/gitignore 2 | # https://help.github.com/articles/ignoring-files 3 | # Example .gitignore files: https://github.com/github/gitignore 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /testcase-request.coffee: -------------------------------------------------------------------------------- 1 | request = require "request" 2 | CombinedStream = require "./lib/combined-stream2.coffee" 3 | str = require "stream" 4 | 5 | stream = CombinedStream.create() 6 | stream.append request("http://google.com/") 7 | stream.append new Buffer("\nDONE!\n") 8 | stream.pipe(new str.PassThrough).resume() 9 | -------------------------------------------------------------------------------- /test.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | CombinedStream = require "./" 3 | devNull = require "dev-null" 4 | 5 | combinedStream = CombinedStream.create() 6 | combinedStream.append fs.createReadStream("./package.json") 7 | combinedStream.append fs.createReadStream("./test.coffee") 8 | #combinedStream.pipe devNull() 9 | combinedStream.pipe process.stdout 10 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | /* CoffeeScript compile deps */ 4 | var path = require('path'); 5 | var gutil = require('gulp-util'); 6 | var concat = require('gulp-concat'); 7 | var rename = require('gulp-rename'); 8 | var coffee = require('gulp-coffee'); 9 | var cache = require('gulp-cached'); 10 | var remember = require('gulp-remember'); 11 | var plumber = require('gulp-plumber'); 12 | 13 | var source = ["lib/**/*.coffee", "index.coffee"] 14 | 15 | gulp.task('coffee', function() { 16 | return gulp.src(source, {base: "."}) 17 | .pipe(plumber()) 18 | .pipe(cache("coffee")) 19 | .pipe(coffee({bare: true}).on('error', gutil.log)).on('data', gutil.log) 20 | .pipe(remember("coffee")) 21 | .pipe(gulp.dest(".")); 22 | }); 23 | 24 | gulp.task('watch', function () { 25 | gulp.watch(source, ['coffee']); 26 | }); 27 | 28 | gulp.task('default', ['coffee', 'watch']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "combined-stream2", 3 | "version": "1.1.2", 4 | "description": "A drop-in Streams2-compatible replacement for combined-stream.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/joepie91/node-combined-stream2" 12 | }, 13 | "keywords": [ 14 | "stream", 15 | "combined", 16 | "multistream", 17 | "streams2" 18 | ], 19 | "author": "Sven Slootweg", 20 | "license": "WTFPL", 21 | "devDependencies": { 22 | "dev-null": "^0.1.1", 23 | "gulp": "~3.8.0", 24 | "gulp-cached": "~0.0.3", 25 | "gulp-coffee": "~2.0.1", 26 | "gulp-concat": "~2.2.0", 27 | "gulp-livereload": "~2.1.0", 28 | "gulp-nodemon": "~1.0.4", 29 | "gulp-plumber": "~0.6.3", 30 | "gulp-remember": "~0.2.0", 31 | "gulp-rename": "~1.2.0", 32 | "gulp-util": "~2.2.17", 33 | "request": "^2.53.0" 34 | }, 35 | "dependencies": { 36 | "debug": "^2.1.1", 37 | "bluebird": "^2.8.1", 38 | "stream-length": "^1.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # combined-stream2 2 | 3 | A drop-in Streams2-compatible replacement for the `combined-stream` module. 4 | 5 | Supports most of the `combined-stream` API. Automatically wraps Streams1 streams, so that they work as well. 6 | 7 | Both Promises and nodebacks are supported. 8 | 9 | ## License 10 | 11 | [WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), whichever you prefer. A donation and/or attribution are appreciated, but not required. 12 | 13 | ## Donate 14 | 15 | My income consists entirely of donations for my projects. If this module is useful to you, consider [making a donation](http://cryto.net/~joepie91/donate.html)! 16 | 17 | You can donate using Bitcoin, PayPal, Gratipay, Flattr, cash-in-mail, SEPA transfers, and pretty much anything else. 18 | 19 | ## Contributing 20 | 21 | Pull requests welcome. Please make sure your modifications are in line with the overall code style, and ensure that you're editing the `.coffee` files, not the `.js` files. 22 | 23 | Build tool of choice is `gulp`; simply run `gulp` while developing, and it will watch for changes. 24 | 25 | Be aware that by making a pull request, you agree to release your modifications under the licenses stated above. 26 | 27 | ## Migrating from `combined-stream` 28 | 29 | Note that there are a few important differences between `combined-stream` and `combined-stream2`: 30 | 31 | * You cannot supply strings, only Buffers and streams. This is because `combined-stream2` doesn't know what the encoding of your string would be, and can't guess safely. You will need to manually encode strings to a Buffer before passing them on to `combined-stream2`. 32 | * The `pauseStreams` option does not exist. All streams are read lazily in non-flowing mode; that is, no data is read until something explicitly tries to read the combined stream. 33 | * The `maxDataSize` option does not exist. 34 | * The `.write()`, `.end()` and `.destroy()` methods are not (yet) implemented. 35 | * There is a `.getCombinedStreamLength()` method that asynchronously returns the total length of all streams (or an error if it cannot be determined). __This method will 'resolve' all callback-supplied streams, as if the stream were being read.__ 36 | 37 | Most usecases will not be affected by these differences, but your mileage may vary. 38 | 39 | ## Using `combined-stream2` with `request` 40 | 41 | __There's an important caveat when trying to append a `request` stream to a `combined-stream2`.__ 42 | 43 | Because `request` does a bunch of strange non-standard hackery with streams, it doesn't play nicely with `combined-stream2` (and many other writable/transform streams). For convenience, `combined-stream2` contains a built-in workaround using a `PassThrough` stream specifically for dealing with `request` streams, but __this workaround will likely result in the entire response being buffered into memory.__ 44 | 45 | Passing in response objects (that is, the object provided in the `response` event handler for a `request` call) is *completely unsupported* - trying to do so will likely break `combined-stream2`. You *must* pass in the `request` object, rather than the `response` object. 46 | 47 | I seriously suggest you consider using [`bhttp`](https://www.npmjs.com/package/bhttp) instead - it has much more predictable behaviour. 48 | 49 | ## Usage 50 | 51 | ```javascript 52 | var CombinedStream = require('combined-stream2'); 53 | var fs = require('fs'); 54 | 55 | var combinedStream = CombinedStream.create(); 56 | combinedStream.append(fs.createReadStream('file1.txt')); 57 | combinedStream.append(fs.createReadStream('file2.txt')); 58 | 59 | combinedStream.pipe(fs.createWriteStream('combined.txt')); 60 | ``` 61 | 62 | ### Appending a stream asynchronously (lazily) 63 | 64 | The function will only be called when you try to either read/pipe the combined stream, or retrieve the total stream length. 65 | ```javascript 66 | combinedStream.append(function(next){ 67 | next(fs.createReadStream('file3.txt')); 68 | }); 69 | ``` 70 | 71 | ### Getting the combined length of all streams 72 | 73 | See the API documentation below for more details. 74 | 75 | ```javascript 76 | Promise = require("bluebird"); 77 | 78 | Promise.try(function(){ 79 | combinedStream.getCombinedStreamLength() 80 | }).then(function(length){ 81 | console.log("Total stream length is " + length + " bytes."); 82 | }).catch(function(err){ 83 | console.log("Could not determine the total stream length!"); 84 | }); 85 | ``` 86 | 87 | ... or using nodebacks: 88 | 89 | ```javascript 90 | combinedStream.getCombinedStreamLength(function(err, length){ 91 | if(err) { 92 | console.log("Could not determine the total stream length!"); 93 | } else { 94 | console.log("Total stream length is " + length + " bytes."); 95 | } 96 | }); 97 | ``` 98 | 99 | ## API 100 | 101 | Since `combined-stream2` is a `stream.Readable`, it inherits the [regular Readable stream properties](http://nodejs.org/api/stream.html#stream_class_stream_readable). Aside from that, the following methods exist: 102 | 103 | ### CombinedStream.create() 104 | 105 | Creates and returns a new `combinedStream`. Contrary to the `.create()` method for the original `combined-stream` module, this method does not accept options. 106 | 107 | ### combinedStream.append(source, [options]) 108 | 109 | Adds a source to the combined stream. Valid sources are streams, Buffers, and callbacks that return either of the two (asynchronously). 110 | 111 | * __source__: The source to add. 112 | * __options__: *Optional.* Additional stream options. 113 | * __contentLength__: The length of the stream. Useful if your stream type is not supported by [`stream-length`](https://www.npmjs.com/package/stream-length), but you know the length of the stream in advance. Also available as `knownLength` for backwards compatibility reasons. 114 | 115 | ### combinedStream.getCombinedStreamLength([callback]) 116 | 117 | __This method will 'resolve' all callback-supplied streams, as if the stream were being read.__ 118 | 119 | Asynchronously returns the total length of all streams (and Buffers) together. If the total length cannot be determined (ie. at least one of the streams is of an unsupported type), an error is thrown asynchronously. 120 | 121 | If you specify a `callback`, it will be treated as a nodeback. If you do *not* specify a `callback`, a Promise will be returned. 122 | 123 | This functionality uses the [`stream-length`](https://www.npmjs.com/package/stream-length) module. 124 | 125 | ### combinedStream.pipe(target) 126 | 127 | Like for other Streams2 Readable streams, this will start piping the combined stream contents into the `target` stream. 128 | 129 | After calling this, you can no longer append new streams. 130 | -------------------------------------------------------------------------------- /lib/combined-stream2.coffee: -------------------------------------------------------------------------------- 1 | stream = require "stream" 2 | Promise = require "bluebird" 3 | streamLength = require "stream-length" 4 | debug = require("debug")("combined-stream2") 5 | 6 | # FIXME: .error handler on streams? 7 | 8 | # Utility functions 9 | ofTypes = (obj, types) -> 10 | match = false 11 | for type in types 12 | match = match or obj instanceof type 13 | return match 14 | 15 | isStream = (obj) -> 16 | return ofTypes obj, [stream.Readable, stream.Duplex, stream.Transform, stream.Stream] 17 | 18 | makeStreams2 = (sourceStream) -> 19 | # Adapted from https://github.com/feross/multistream/blob/master/index.js 20 | if not sourceStream or typeof sourceStream == "function" or sourceStream instanceof Buffer or sourceStream._readableState? 21 | debug "already streams2 or otherwise compatible" 22 | return sourceStream 23 | 24 | if sourceStream.httpModule? 25 | # This is a special case for `request`, because it does weird stream hackery. 26 | # NOTE: The caveat is that this will buffer up in memory. 27 | debug "found `request` stream, using PassThrough stream..." 28 | return sourceStream.pipe(new stream.PassThrough()) 29 | 30 | debug "wrapping stream..." 31 | wrapper = new stream.Readable().wrap(sourceStream) 32 | 33 | if sourceStream.destroy? 34 | wrapper.destroy = sourceStream.destroy.bind(sourceStream) 35 | 36 | debug "returning streams2-wrapped stream" 37 | return wrapper 38 | 39 | # The actual stream class definition 40 | class CombinedStream extends stream.Readable 41 | constructor: -> 42 | super 43 | @_reading = false 44 | @_sources = [] 45 | @_currentSource = null 46 | @_sourceDataAvailable = false 47 | @_wantData = false 48 | 49 | append: (source, options = {}) -> 50 | # Only readable binary data sources are allowed. 51 | if not ofTypes source, [stream.Readable, stream.Duplex, stream.Transform, stream.Stream, Buffer, Function] 52 | throw new Error "The provided source must be either a readable stream or a Buffer, or a callback providing either of those. If it is currently a string, you need to convert it to a Buffer yourself and ensure that the encoding is correct." 53 | 54 | debug "appending source: %s", source.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r") 55 | @_sources.push [makeStreams2(source), options] 56 | 57 | getStreamLengths: -> 58 | debug "getting stream lengths" 59 | if @_reading 60 | Promise.reject new Error("You can't obtain the stream lengths anymore once you've started reading!") 61 | else 62 | Promise.try => 63 | @_resolveAllSources() 64 | .then (actualSources) => 65 | @_sources = actualSources 66 | Promise.resolve actualSources 67 | .map (source) -> 68 | if source[1]?.knownLength? or source[1]?.contentLength? 69 | Promise.resolve source[1]?.knownLength ? source[1]?.contentLength 70 | else 71 | streamLength source[0] 72 | 73 | getCombinedStreamLength: (callback) -> 74 | debug "getting combined stream length" 75 | Promise.try => 76 | @getStreamLengths() 77 | .reduce ((total, current) -> total + current), 0 78 | .nodeify(callback) 79 | 80 | _resolveAllSources: -> 81 | debug "resolving all sources" 82 | Promise.all (@_resolveSource(source) for source in @_sources) 83 | 84 | _resolveSource: (source) -> 85 | # If the 'source' is a function, then it's actually a callback that will *return* the source. We call the callback, and supply it with a `next` function that will post-process the source, and eventually trigger the actual read. 86 | new Promise (resolve, reject) => # WARN? 87 | if source[0] instanceof Function 88 | debug "resolving %s", source[0].toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r") 89 | source[0] (realSource) => 90 | resolve [realSource, source[1]] 91 | else 92 | # It's a regular source, so we immediately continue. 93 | debug "source %s is already resolved", source[0].toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r") 94 | resolve source 95 | 96 | _initiateRead: -> 97 | Promise.try => 98 | @_reading = true 99 | @_resolveAllSources() 100 | .then (actualSources) => 101 | @_sources = actualSources 102 | Promise.resolve() 103 | 104 | _read: (size) -> 105 | Promise.try => 106 | if @_reading == false 107 | @_initiateRead() 108 | else 109 | Promise.resolve() 110 | .then => 111 | @_doRead size 112 | 113 | _doRead: (size) -> 114 | # FIXME: We should probably try to do something with `size` ourselves. Just passing it on for now, but it'd be nice to implement it properly in the future - this might help efficiency in some cases. 115 | Promise.try => 116 | if @_currentSource == null 117 | # We're not currently actively reading from any sources. Set a new source to be the current source. 118 | @_nextSource size 119 | else 120 | # We haven't changed our source - immediately continue with the actual read. 121 | Promise.resolve() 122 | .then => 123 | @_doActualRead size 124 | 125 | _nextSource: (readSize) -> 126 | if @_sources.length == 0 127 | # We've run out of sources - signal EOF and bail. 128 | debug "ran out of streams; pushing EOF" 129 | @push null 130 | return 131 | 132 | @_currentSource = @_sources.shift()[0] 133 | @_currentIsStream = isStream @_currentSource 134 | debug "switching to new source (stream = %s): %s", @_currentIsStream, @_currentSource.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r") 135 | 136 | if @_currentIsStream 137 | @_currentSource.once "end", => 138 | # We've depleted the stream (ie. we've read 'null') The current source should be set to `null`, so that on the next read a new source will be picked. We'll also immediately trigger the next read - the stream will be expecting to receive *some* kind of data before calling the next read itself. 139 | @_currentSource = null 140 | @_doRead readSize # FIXME: This should probably use the last-requested read size, not the one that was requested when *setting up* the `end` event. 141 | 142 | @_currentSource.on "readable", => 143 | debug "received readable event, setting sourceDataAvailable to true" 144 | @_sourceDataAvailable = true 145 | 146 | if @_wantData 147 | debug "wantData queued, reading" 148 | @_doStreamRead() 149 | 150 | Promise.resolve() 151 | 152 | # We're wrapping the actual reading code in a separate function, so as to facilitate source-returning callbacks in the sources list. 153 | _doActualRead: (size) => 154 | # FIXME: Apparently, it may be possible to push more than one chunk in a single _read call. The implementation specifics of this should probably be looked into - that could perhaps make our stream a bit more efficient. On the other hand, shouldn't we leave this for the Writable to decide? 155 | new Promise (resolve, reject) => 156 | if @_currentIsStream 157 | # This is a readable stream of some sort - we'll do a read, and pass on the result. We'll pass on the `size` parameter, but there's no guarantee that anything will actually be done with it. 158 | if @_sourceDataAvailable 159 | @_doStreamRead() 160 | return resolve() 161 | else 162 | debug "want data, but no readable event fired yet, setting wantData to true" 163 | @_wantData = true 164 | return resolve() # We haven't actually read anything yet, but whatever. 165 | else 166 | # This is a Buffer - we'll push it as is, and immediately mark it as completed. 167 | chunk = @_currentSource 168 | 169 | # We need to unset it *before* pushing the chunk, because otherwise V8 will sometimes not give control back to this function, and a second read may occur before the source can be unset. 170 | @_currentSource = null 171 | 172 | if chunk != null # FIXME: ??? 173 | debug "pushing buffer %s", chunk.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r") 174 | @push chunk 175 | else 176 | debug "WARN: current source was null, pushing empty buffer" 177 | @push new Buffer("") 178 | 179 | resolve() 180 | 181 | _doStreamRead: => 182 | Promise.try => 183 | @_sourceDataAvailable = false 184 | @_wantData = false 185 | chunk = @_currentSource.read() 186 | 187 | # Since Node.js v0.12, a stream will apparently return null when it is finished... we need to filter this out, to prevent it from ending our combined stream prematurely. 188 | if chunk? 189 | @push chunk 190 | 191 | Promise.resolve() 192 | 193 | # Public module API 194 | module.exports = 195 | create: (options) -> 196 | # We implement the same API as the original `combined-stream`, for drop-in compatibility reasons. 197 | return new CombinedStream(options) 198 | -------------------------------------------------------------------------------- /lib/combined-stream2.js: -------------------------------------------------------------------------------- 1 | var CombinedStream, Promise, debug, isStream, makeStreams2, ofTypes, stream, streamLength, 2 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 5 | 6 | stream = require("stream"); 7 | 8 | Promise = require("bluebird"); 9 | 10 | streamLength = require("stream-length"); 11 | 12 | debug = require("debug")("combined-stream2"); 13 | 14 | ofTypes = function(obj, types) { 15 | var match, type, _i, _len; 16 | match = false; 17 | for (_i = 0, _len = types.length; _i < _len; _i++) { 18 | type = types[_i]; 19 | match = match || obj instanceof type; 20 | } 21 | return match; 22 | }; 23 | 24 | isStream = function(obj) { 25 | return ofTypes(obj, [stream.Readable, stream.Duplex, stream.Transform, stream.Stream]); 26 | }; 27 | 28 | makeStreams2 = function(sourceStream) { 29 | var wrapper; 30 | if (!sourceStream || typeof sourceStream === "function" || sourceStream instanceof Buffer || (sourceStream._readableState != null)) { 31 | debug("already streams2 or otherwise compatible"); 32 | return sourceStream; 33 | } 34 | if (sourceStream.httpModule != null) { 35 | debug("found `request` stream, using PassThrough stream..."); 36 | return sourceStream.pipe(new stream.PassThrough()); 37 | } 38 | debug("wrapping stream..."); 39 | wrapper = new stream.Readable().wrap(sourceStream); 40 | if (sourceStream.destroy != null) { 41 | wrapper.destroy = sourceStream.destroy.bind(sourceStream); 42 | } 43 | debug("returning streams2-wrapped stream"); 44 | return wrapper; 45 | }; 46 | 47 | CombinedStream = (function(_super) { 48 | __extends(CombinedStream, _super); 49 | 50 | function CombinedStream() { 51 | this._doStreamRead = __bind(this._doStreamRead, this); 52 | this._doActualRead = __bind(this._doActualRead, this); 53 | CombinedStream.__super__.constructor.apply(this, arguments); 54 | this._reading = false; 55 | this._sources = []; 56 | this._currentSource = null; 57 | this._sourceDataAvailable = false; 58 | this._wantData = false; 59 | } 60 | 61 | CombinedStream.prototype.append = function(source, options) { 62 | if (options == null) { 63 | options = {}; 64 | } 65 | if (!ofTypes(source, [stream.Readable, stream.Duplex, stream.Transform, stream.Stream, Buffer, Function])) { 66 | throw new Error("The provided source must be either a readable stream or a Buffer, or a callback providing either of those. If it is currently a string, you need to convert it to a Buffer yourself and ensure that the encoding is correct."); 67 | } 68 | debug("appending source: %s", source.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")); 69 | return this._sources.push([makeStreams2(source), options]); 70 | }; 71 | 72 | CombinedStream.prototype.getStreamLengths = function() { 73 | debug("getting stream lengths"); 74 | if (this._reading) { 75 | return Promise.reject(new Error("You can't obtain the stream lengths anymore once you've started reading!")); 76 | } else { 77 | return Promise["try"]((function(_this) { 78 | return function() { 79 | return _this._resolveAllSources(); 80 | }; 81 | })(this)).then((function(_this) { 82 | return function(actualSources) { 83 | _this._sources = actualSources; 84 | return Promise.resolve(actualSources); 85 | }; 86 | })(this)).map(function(source) { 87 | var _ref, _ref1, _ref2, _ref3, _ref4; 88 | if ((((_ref = source[1]) != null ? _ref.knownLength : void 0) != null) || (((_ref1 = source[1]) != null ? _ref1.contentLength : void 0) != null)) { 89 | return Promise.resolve((_ref2 = (_ref3 = source[1]) != null ? _ref3.knownLength : void 0) != null ? _ref2 : (_ref4 = source[1]) != null ? _ref4.contentLength : void 0); 90 | } else { 91 | return streamLength(source[0]); 92 | } 93 | }); 94 | } 95 | }; 96 | 97 | CombinedStream.prototype.getCombinedStreamLength = function(callback) { 98 | debug("getting combined stream length"); 99 | return Promise["try"]((function(_this) { 100 | return function() { 101 | return _this.getStreamLengths(); 102 | }; 103 | })(this)).reduce((function(total, current) { 104 | return total + current; 105 | }), 0).nodeify(callback); 106 | }; 107 | 108 | CombinedStream.prototype._resolveAllSources = function() { 109 | var source; 110 | debug("resolving all sources"); 111 | return Promise.all((function() { 112 | var _i, _len, _ref, _results; 113 | _ref = this._sources; 114 | _results = []; 115 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 116 | source = _ref[_i]; 117 | _results.push(this._resolveSource(source)); 118 | } 119 | return _results; 120 | }).call(this)); 121 | }; 122 | 123 | CombinedStream.prototype._resolveSource = function(source) { 124 | return new Promise((function(_this) { 125 | return function(resolve, reject) { 126 | if (source[0] instanceof Function) { 127 | debug("resolving %s", source[0].toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")); 128 | return source[0](function(realSource) { 129 | return resolve([realSource, source[1]]); 130 | }); 131 | } else { 132 | debug("source %s is already resolved", source[0].toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")); 133 | return resolve(source); 134 | } 135 | }; 136 | })(this)); 137 | }; 138 | 139 | CombinedStream.prototype._initiateRead = function() { 140 | return Promise["try"]((function(_this) { 141 | return function() { 142 | _this._reading = true; 143 | return _this._resolveAllSources(); 144 | }; 145 | })(this)).then((function(_this) { 146 | return function(actualSources) { 147 | _this._sources = actualSources; 148 | return Promise.resolve(); 149 | }; 150 | })(this)); 151 | }; 152 | 153 | CombinedStream.prototype._read = function(size) { 154 | return Promise["try"]((function(_this) { 155 | return function() { 156 | if (_this._reading === false) { 157 | return _this._initiateRead(); 158 | } else { 159 | return Promise.resolve(); 160 | } 161 | }; 162 | })(this)).then((function(_this) { 163 | return function() { 164 | return _this._doRead(size); 165 | }; 166 | })(this)); 167 | }; 168 | 169 | CombinedStream.prototype._doRead = function(size) { 170 | return Promise["try"]((function(_this) { 171 | return function() { 172 | if (_this._currentSource === null) { 173 | return _this._nextSource(size); 174 | } else { 175 | return Promise.resolve(); 176 | } 177 | }; 178 | })(this)).then((function(_this) { 179 | return function() { 180 | return _this._doActualRead(size); 181 | }; 182 | })(this)); 183 | }; 184 | 185 | CombinedStream.prototype._nextSource = function(readSize) { 186 | if (this._sources.length === 0) { 187 | debug("ran out of streams; pushing EOF"); 188 | this.push(null); 189 | return; 190 | } 191 | this._currentSource = this._sources.shift()[0]; 192 | this._currentIsStream = isStream(this._currentSource); 193 | debug("switching to new source (stream = %s): %s", this._currentIsStream, this._currentSource.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")); 194 | if (this._currentIsStream) { 195 | this._currentSource.once("end", (function(_this) { 196 | return function() { 197 | _this._currentSource = null; 198 | return _this._doRead(readSize); 199 | }; 200 | })(this)); 201 | this._currentSource.on("readable", (function(_this) { 202 | return function() { 203 | debug("received readable event, setting sourceDataAvailable to true"); 204 | _this._sourceDataAvailable = true; 205 | if (_this._wantData) { 206 | debug("wantData queued, reading"); 207 | return _this._doStreamRead(); 208 | } 209 | }; 210 | })(this)); 211 | } 212 | return Promise.resolve(); 213 | }; 214 | 215 | CombinedStream.prototype._doActualRead = function(size) { 216 | return new Promise((function(_this) { 217 | return function(resolve, reject) { 218 | var chunk; 219 | if (_this._currentIsStream) { 220 | if (_this._sourceDataAvailable) { 221 | _this._doStreamRead(); 222 | return resolve(); 223 | } else { 224 | debug("want data, but no readable event fired yet, setting wantData to true"); 225 | _this._wantData = true; 226 | return resolve(); 227 | } 228 | } else { 229 | chunk = _this._currentSource; 230 | _this._currentSource = null; 231 | if (chunk !== null) { 232 | debug("pushing buffer %s", chunk.toString().replace(/\n/g, "\\n").replace(/\r/g, "\\r")); 233 | _this.push(chunk); 234 | } else { 235 | debug("WARN: current source was null, pushing empty buffer"); 236 | _this.push(new Buffer("")); 237 | } 238 | return resolve(); 239 | } 240 | }; 241 | })(this)); 242 | }; 243 | 244 | CombinedStream.prototype._doStreamRead = function() { 245 | return Promise["try"]((function(_this) { 246 | return function() { 247 | var chunk; 248 | _this._sourceDataAvailable = false; 249 | _this._wantData = false; 250 | chunk = _this._currentSource.read(); 251 | if (chunk != null) { 252 | _this.push(chunk); 253 | } 254 | return Promise.resolve(); 255 | }; 256 | })(this)); 257 | }; 258 | 259 | return CombinedStream; 260 | 261 | })(stream.Readable); 262 | 263 | module.exports = { 264 | create: function(options) { 265 | return new CombinedStream(options); 266 | } 267 | }; 268 | --------------------------------------------------------------------------------