├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 23 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | coverage 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {type Readable} from 'node:stream'; 2 | 3 | /** 4 | Merges an array of [readable streams](https://nodejs.org/api/stream.html#readable-streams) and returns a new readable stream that emits data from the individual streams as it arrives. 5 | 6 | If you provide an empty array, the stream remains open but can be [manually ended](https://nodejs.org/api/stream.html#writableendchunk-encoding-callback). 7 | 8 | @example 9 | ``` 10 | import mergeStreams from '@sindresorhus/merge-streams'; 11 | 12 | const stream = mergeStreams([streamA, streamB]); 13 | 14 | for await (const chunk of stream) { 15 | console.log(chunk); 16 | //=> 'A1' 17 | //=> 'B1' 18 | //=> 'A2' 19 | //=> 'B2' 20 | } 21 | ``` 22 | */ 23 | export default function mergeStreams(streams: Readable[]): MergedStream; 24 | 25 | /** 26 | A single stream combining the output of multiple streams. 27 | */ 28 | export class MergedStream extends Readable { 29 | /** 30 | Pipe a new readable stream. 31 | 32 | Throws if `MergedStream` has already ended. 33 | */ 34 | add(stream: Readable): void; 35 | 36 | /** 37 | Unpipe a stream previously added using either `mergeStreams(streams)` or `MergedStream.add(stream)`. 38 | 39 | Returns `false` if the stream was not previously added, or if it was already removed by `MergedStream.remove(stream)`. 40 | 41 | The removed stream is not automatically ended. 42 | */ 43 | remove(stream: Readable): Promise; 44 | } 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {on, once} from 'node:events'; 2 | import {PassThrough as PassThroughStream, getDefaultHighWaterMark} from 'node:stream'; 3 | import {finished} from 'node:stream/promises'; 4 | 5 | export default function mergeStreams(streams) { 6 | if (!Array.isArray(streams)) { 7 | throw new TypeError(`Expected an array, got \`${typeof streams}\`.`); 8 | } 9 | 10 | for (const stream of streams) { 11 | validateStream(stream); 12 | } 13 | 14 | const objectMode = streams.some(({readableObjectMode}) => readableObjectMode); 15 | const highWaterMark = getHighWaterMark(streams, objectMode); 16 | const passThroughStream = new MergedStream({ 17 | objectMode, 18 | writableHighWaterMark: highWaterMark, 19 | readableHighWaterMark: highWaterMark, 20 | }); 21 | 22 | for (const stream of streams) { 23 | passThroughStream.add(stream); 24 | } 25 | 26 | return passThroughStream; 27 | } 28 | 29 | const getHighWaterMark = (streams, objectMode) => { 30 | if (streams.length === 0) { 31 | return getDefaultHighWaterMark(objectMode); 32 | } 33 | 34 | const highWaterMarks = streams 35 | .filter(({readableObjectMode}) => readableObjectMode === objectMode) 36 | .map(({readableHighWaterMark}) => readableHighWaterMark); 37 | return Math.max(...highWaterMarks); 38 | }; 39 | 40 | class MergedStream extends PassThroughStream { 41 | #streams = new Set([]); 42 | #ended = new Set([]); 43 | #aborted = new Set([]); 44 | #onFinished; 45 | #unpipeEvent = Symbol('unpipe'); 46 | #streamPromises = new WeakMap(); 47 | 48 | add(stream) { 49 | validateStream(stream); 50 | 51 | if (this.#streams.has(stream)) { 52 | return; 53 | } 54 | 55 | this.#streams.add(stream); 56 | 57 | this.#onFinished ??= onMergedStreamFinished(this, this.#streams, this.#unpipeEvent); 58 | const streamPromise = endWhenStreamsDone({ 59 | passThroughStream: this, 60 | stream, 61 | streams: this.#streams, 62 | ended: this.#ended, 63 | aborted: this.#aborted, 64 | onFinished: this.#onFinished, 65 | unpipeEvent: this.#unpipeEvent, 66 | }); 67 | this.#streamPromises.set(stream, streamPromise); 68 | 69 | stream.pipe(this, {end: false}); 70 | } 71 | 72 | async remove(stream) { 73 | validateStream(stream); 74 | 75 | if (!this.#streams.has(stream)) { 76 | return false; 77 | } 78 | 79 | const streamPromise = this.#streamPromises.get(stream); 80 | if (streamPromise === undefined) { 81 | return false; 82 | } 83 | 84 | this.#streamPromises.delete(stream); 85 | 86 | stream.unpipe(this); 87 | await streamPromise; 88 | return true; 89 | } 90 | } 91 | 92 | const onMergedStreamFinished = async (passThroughStream, streams, unpipeEvent) => { 93 | updateMaxListeners(passThroughStream, PASSTHROUGH_LISTENERS_COUNT); 94 | const controller = new AbortController(); 95 | 96 | try { 97 | await Promise.race([ 98 | onMergedStreamEnd(passThroughStream, controller), 99 | onInputStreamsUnpipe(passThroughStream, streams, unpipeEvent, controller), 100 | ]); 101 | } finally { 102 | controller.abort(); 103 | updateMaxListeners(passThroughStream, -PASSTHROUGH_LISTENERS_COUNT); 104 | } 105 | }; 106 | 107 | const onMergedStreamEnd = async (passThroughStream, {signal}) => { 108 | try { 109 | await finished(passThroughStream, {signal, cleanup: true}); 110 | } catch (error) { 111 | errorOrAbortStream(passThroughStream, error); 112 | throw error; 113 | } 114 | }; 115 | 116 | const onInputStreamsUnpipe = async (passThroughStream, streams, unpipeEvent, {signal}) => { 117 | for await (const [unpipedStream] of on(passThroughStream, 'unpipe', {signal})) { 118 | if (streams.has(unpipedStream)) { 119 | unpipedStream.emit(unpipeEvent); 120 | } 121 | } 122 | }; 123 | 124 | const validateStream = stream => { 125 | if (typeof stream?.pipe !== 'function') { 126 | throw new TypeError(`Expected a readable stream, got: \`${typeof stream}\`.`); 127 | } 128 | }; 129 | 130 | const endWhenStreamsDone = async ({passThroughStream, stream, streams, ended, aborted, onFinished, unpipeEvent}) => { 131 | updateMaxListeners(passThroughStream, PASSTHROUGH_LISTENERS_PER_STREAM); 132 | const controller = new AbortController(); 133 | 134 | try { 135 | await Promise.race([ 136 | afterMergedStreamFinished(onFinished, stream, controller), 137 | onInputStreamEnd({ 138 | passThroughStream, 139 | stream, 140 | streams, 141 | ended, 142 | aborted, 143 | controller, 144 | }), 145 | onInputStreamUnpipe({ 146 | stream, 147 | streams, 148 | ended, 149 | aborted, 150 | unpipeEvent, 151 | controller, 152 | }), 153 | ]); 154 | } finally { 155 | controller.abort(); 156 | updateMaxListeners(passThroughStream, -PASSTHROUGH_LISTENERS_PER_STREAM); 157 | } 158 | 159 | if (streams.size > 0 && streams.size === ended.size + aborted.size) { 160 | if (ended.size === 0 && aborted.size > 0) { 161 | abortStream(passThroughStream); 162 | } else { 163 | endStream(passThroughStream); 164 | } 165 | } 166 | }; 167 | 168 | const afterMergedStreamFinished = async (onFinished, stream, {signal}) => { 169 | try { 170 | await onFinished; 171 | if (!signal.aborted) { 172 | abortStream(stream); 173 | } 174 | } catch (error) { 175 | if (!signal.aborted) { 176 | errorOrAbortStream(stream, error); 177 | } 178 | } 179 | }; 180 | 181 | const onInputStreamEnd = async ({passThroughStream, stream, streams, ended, aborted, controller: {signal}}) => { 182 | try { 183 | await finished(stream, { 184 | signal, 185 | cleanup: true, 186 | readable: true, 187 | writable: false, 188 | }); 189 | if (streams.has(stream)) { 190 | ended.add(stream); 191 | } 192 | } catch (error) { 193 | if (signal.aborted || !streams.has(stream)) { 194 | return; 195 | } 196 | 197 | if (isAbortError(error)) { 198 | aborted.add(stream); 199 | } else { 200 | errorStream(passThroughStream, error); 201 | } 202 | } 203 | }; 204 | 205 | const onInputStreamUnpipe = async ({stream, streams, ended, aborted, unpipeEvent, controller: {signal}}) => { 206 | await once(stream, unpipeEvent, {signal}); 207 | 208 | if (!stream.readable) { 209 | return once(signal, 'abort', {signal}); 210 | } 211 | 212 | streams.delete(stream); 213 | ended.delete(stream); 214 | aborted.delete(stream); 215 | }; 216 | 217 | const endStream = stream => { 218 | if (stream.writable) { 219 | stream.end(); 220 | } 221 | }; 222 | 223 | const errorOrAbortStream = (stream, error) => { 224 | if (isAbortError(error)) { 225 | abortStream(stream); 226 | } else { 227 | errorStream(stream, error); 228 | } 229 | }; 230 | 231 | // This is the error thrown by `finished()` on `stream.destroy()` 232 | const isAbortError = error => error?.code === 'ERR_STREAM_PREMATURE_CLOSE'; 233 | 234 | const abortStream = stream => { 235 | if (stream.readable || stream.writable) { 236 | stream.destroy(); 237 | } 238 | }; 239 | 240 | // `stream.destroy(error)` crashes the process with `uncaughtException` if no `error` event listener exists on `stream`. 241 | // We take care of error handling on user behalf, so we do not want this to happen. 242 | const errorStream = (stream, error) => { 243 | if (!stream.destroyed) { 244 | stream.once('error', noop); 245 | stream.destroy(error); 246 | } 247 | }; 248 | 249 | const noop = () => {}; 250 | 251 | const updateMaxListeners = (passThroughStream, increment) => { 252 | const maxListeners = passThroughStream.getMaxListeners(); 253 | if (maxListeners !== 0 && maxListeners !== Number.POSITIVE_INFINITY) { 254 | passThroughStream.setMaxListeners(maxListeners + increment); 255 | } 256 | }; 257 | 258 | // Number of times `passThroughStream.on()` is called regardless of streams: 259 | // - once due to `finished(passThroughStream)` 260 | // - once due to `on(passThroughStream)` 261 | const PASSTHROUGH_LISTENERS_COUNT = 2; 262 | 263 | // Number of times `passThroughStream.on()` is called per stream: 264 | // - once due to `stream.pipe(passThroughStream)` 265 | const PASSTHROUGH_LISTENERS_PER_STREAM = 1; 266 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {Readable} from 'node:stream'; 2 | import {expectType, expectError, expectAssignable} from 'tsd'; 3 | import mergeStreams, {type MergedStream} from './index.js'; 4 | 5 | const readableStream = Readable.from('.'); 6 | 7 | expectType(mergeStreams([])); 8 | expectAssignable(mergeStreams([])); 9 | expectAssignable(mergeStreams([readableStream])); 10 | 11 | expectError(mergeStreams()); 12 | expectError(mergeStreams(readableStream)); 13 | expectError(mergeStreams([''])); 14 | 15 | const mergedStream = mergeStreams([]); 16 | expectType(mergedStream.add(readableStream)); 17 | expectError(mergedStream.add()); 18 | expectError(mergedStream.add([])); 19 | expectError(mergedStream.add('')); 20 | 21 | expectType>(mergedStream.remove(readableStream)); 22 | expectError(await mergedStream.remove()); 23 | expectError(await mergedStream.remove([])); 24 | expectError(await mergedStream.remove('')); 25 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sindresorhus/merge-streams", 3 | "version": "4.0.0", 4 | "description": "Merge multiple streams into a unified stream", 5 | "license": "MIT", 6 | "repository": "sindresorhus/merge-streams", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18.18" 21 | }, 22 | "scripts": { 23 | "test": "xo && c8 ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "merge", 31 | "stream", 32 | "streams", 33 | "readable", 34 | "passthrough", 35 | "interleave", 36 | "interleaved", 37 | "unify", 38 | "unified" 39 | ], 40 | "devDependencies": { 41 | "@types/node": "^20.8.9", 42 | "ava": "^6.1.0", 43 | "c8": "^9.1.0", 44 | "tempfile": "^5.0.0", 45 | "tsd": "^0.31.0", 46 | "typescript": "^5.2.2", 47 | "xo": "^0.58.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # merge-streams 2 | 3 | > Merge multiple streams into a unified stream 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install @sindresorhus/merge-streams 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import mergeStreams from '@sindresorhus/merge-streams'; 15 | 16 | const stream = mergeStreams([streamA, streamB]); 17 | 18 | for await (const chunk of stream) { 19 | console.log(chunk); 20 | //=> 'A1' 21 | //=> 'B1' 22 | //=> 'A2' 23 | //=> 'B2' 24 | } 25 | ``` 26 | 27 | ## API 28 | 29 | ### `mergeStreams(streams: stream.Readable[]): MergedStream` 30 | 31 | Merges an array of [readable streams](https://nodejs.org/api/stream.html#readable-streams) and returns a new readable stream that emits data from the individual streams as it arrives. 32 | 33 | If you provide an empty array, the stream remains open but can be [manually ended](https://nodejs.org/api/stream.html#writableendchunk-encoding-callback). 34 | 35 | #### `MergedStream` 36 | 37 | _Type_: `stream.Readable` 38 | 39 | A single stream combining the output of multiple streams. 40 | 41 | ##### `MergedStream.add(stream: stream.Readable): void` 42 | 43 | Pipe a new readable stream. 44 | 45 | Throws if `MergedStream` has already ended. 46 | 47 | ##### `MergedStream.remove(stream: stream.Readable): Promise` 48 | 49 | Unpipe a stream previously added using either [`mergeStreams(streams)`](#mergestreamsstreams-streamreadable-mergedstream) or [`MergedStream.add(stream)`](#mergedstreamaddstream-streamreadable-void). 50 | 51 | Returns `false` if the stream was not previously added, or if it was already removed by `MergedStream.remove(stream)`. 52 | 53 | The removed stream is not automatically ended. 54 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import {once, defaultMaxListeners} from 'node:events'; 2 | import {createReadStream} from 'node:fs'; 3 | import {writeFile, rm} from 'node:fs/promises'; 4 | import {Readable, PassThrough, getDefaultHighWaterMark} from 'node:stream'; 5 | import {text} from 'node:stream/consumers'; 6 | import {scheduler} from 'node:timers/promises'; 7 | import test from 'ava'; 8 | import tempfile from 'tempfile'; 9 | import mergeStreams from './index.js'; 10 | 11 | const getInfiniteStream = () => new Readable({read() {}}); 12 | 13 | const prematureClose = {code: 'ERR_STREAM_PREMATURE_CLOSE'}; 14 | 15 | test('Works with Readable.from()', async t => { 16 | const stream = mergeStreams([ 17 | Readable.from(['a', 'b']), 18 | Readable.from(['c', 'd']), 19 | ]); 20 | 21 | const result = await stream.toArray(); 22 | t.deepEqual(result.sort(), ['a', 'b', 'c', 'd'].sort()); 23 | }); 24 | 25 | const bigContents = '.'.repeat(1e6); 26 | 27 | test('Works with fs.createReadStream()', async t => { 28 | const files = [tempfile(), tempfile()]; 29 | await Promise.all(files.map(file => writeFile(file, bigContents))); 30 | 31 | const stream = mergeStreams(files.map(file => createReadStream(file, 'utf8'))); 32 | 33 | t.is(await text(stream), `${bigContents}${bigContents}`); 34 | 35 | await Promise.all(files.map(file => rm(file))); 36 | }); 37 | 38 | const largeValue = '.'.repeat(1e7); 39 | const largeRepeat = 20; 40 | 41 | test('Handles large values', async t => { 42 | const stream = mergeStreams([Readable.from(Array.from({length: largeRepeat}).fill(largeValue))]); 43 | t.is(await text(stream), largeValue.repeat(largeRepeat)); 44 | }); 45 | 46 | test('Propagate stream destroy with error', async t => { 47 | const inputStream = getInfiniteStream(); 48 | const stream = mergeStreams([inputStream]); 49 | const error = new Error('test'); 50 | inputStream.destroy(error); 51 | const [destinationError] = await once(stream, 'error'); 52 | t.is(destinationError, error); 53 | t.false(stream.readableEnded); 54 | t.is(stream.errored, error); 55 | t.true(stream.readableAborted); 56 | t.true(stream.closed); 57 | t.true(stream.destroyed); 58 | }); 59 | 60 | test('Propagate stream error event', async t => { 61 | const inputStream = getInfiniteStream(); 62 | const stream = mergeStreams([inputStream]); 63 | const error = new Error('test'); 64 | inputStream.emit('error', error); 65 | const [destinationError] = await once(stream, 'error'); 66 | t.is(destinationError, error); 67 | t.false(stream.readableEnded); 68 | t.is(stream.errored, error); 69 | t.true(stream.readableAborted); 70 | t.true(stream.closed); 71 | t.true(stream.destroyed); 72 | }); 73 | 74 | test('Propagate stream abort of all streams', async t => { 75 | const inputStream = getInfiniteStream(); 76 | const stream = mergeStreams([inputStream]); 77 | inputStream.destroy(); 78 | await once(stream, 'close'); 79 | t.false(stream.readableEnded); 80 | t.is(stream.errored, null); 81 | t.true(stream.readableAborted); 82 | t.true(stream.closed); 83 | t.true(stream.destroyed); 84 | }); 85 | 86 | test('Propagate stream abort of some streams', async t => { 87 | const inputStream = getInfiniteStream(); 88 | const stream = mergeStreams([inputStream, Readable.from('.')]); 89 | inputStream.destroy(); 90 | t.is(await text(stream), '.'); 91 | t.true(stream.readableEnded); 92 | t.is(stream.errored, null); 93 | t.false(stream.readableAborted); 94 | t.true(stream.closed); 95 | t.true(stream.destroyed); 96 | }); 97 | 98 | test('Handles no input', async t => { 99 | const stream = mergeStreams([]); 100 | t.false(stream.readableObjectMode); 101 | t.false(stream.writableObjectMode); 102 | t.is(stream.readableHighWaterMark, getDefaultHighWaterMark(false)); 103 | t.is(stream.writableHighWaterMark, getDefaultHighWaterMark(false)); 104 | t.true(stream.writable); 105 | t.true(stream.readable); 106 | 107 | stream.end(); 108 | t.deepEqual(await stream.toArray(), []); 109 | t.false(stream.writable); 110 | t.false(stream.readable); 111 | }); 112 | 113 | test('Can add stream after initially setting to no input', async t => { 114 | const stream = mergeStreams([]); 115 | stream.add(Readable.from('.')); 116 | t.is(await text(stream), '.'); 117 | }); 118 | 119 | test('Validates argument is an array', t => { 120 | t.throws(() => { 121 | mergeStreams(Readable.from('.')); 122 | }, {message: /Expected an array/}); 123 | }); 124 | 125 | test('Validates arguments are streams', t => { 126 | t.throws(() => { 127 | mergeStreams([false]); 128 | }, {message: /Expected a readable stream/}); 129 | }); 130 | 131 | test('Validates add() argument is stream', t => { 132 | t.throws(() => { 133 | mergeStreams([Readable.from('.')]).add(false); 134 | }, {message: /Expected a readable stream/}); 135 | }); 136 | 137 | const testObjectMode = async (t, firstObjectMode, secondObjectMode, mergeObjectMode) => { 138 | const stream = mergeStreams([ 139 | Readable.from('a', {objectMode: firstObjectMode}), 140 | Readable.from('b', {objectMode: secondObjectMode}), 141 | ]); 142 | t.is(stream.readableObjectMode, mergeObjectMode); 143 | await stream.toArray(); 144 | }; 145 | 146 | test('Is not in objectMode if no input stream is', testObjectMode, false, false, false); 147 | test('Is in objectMode if only some input streams are', testObjectMode, false, true, true); 148 | test('Is in objectMode if all input streams are', testObjectMode, true, true, true); 149 | 150 | test('"add()" cannot change objectMode', async t => { 151 | const stream = mergeStreams([Readable.from('.', {objectMode: false})]); 152 | stream.add(Readable.from('.', {objectMode: true})); 153 | t.false(stream.readableObjectMode); 154 | await stream.toArray(); 155 | }); 156 | 157 | test('Can end the merge stream before the input streams', async t => { 158 | const pendingStream = new PassThrough(); 159 | const stream = mergeStreams([pendingStream]); 160 | stream.end(); 161 | t.deepEqual(await stream.toArray(), []); 162 | await t.throwsAsync(pendingStream.toArray(), prematureClose); 163 | }); 164 | 165 | test('Can abort the merge stream before the input streams', async t => { 166 | const pendingStream = new PassThrough(); 167 | const stream = mergeStreams([pendingStream]); 168 | stream.destroy(); 169 | await t.throwsAsync(stream.toArray(), prematureClose); 170 | t.false(pendingStream.readableEnded); 171 | t.is(pendingStream.errored, null); 172 | t.true(pendingStream.destroyed); 173 | }); 174 | 175 | test('Can destroy the merge stream before the input streams', async t => { 176 | const pendingStream = new PassThrough(); 177 | const stream = mergeStreams([pendingStream]); 178 | const error = new Error('test'); 179 | stream.destroy(error); 180 | t.is(await t.throwsAsync(stream.toArray()), error); 181 | t.false(pendingStream.readableEnded); 182 | t.is(pendingStream.errored, error); 183 | t.true(pendingStream.destroyed); 184 | }); 185 | 186 | test('Can end the merge stream with no input streams', async t => { 187 | const stream = mergeStreams([]); 188 | stream.end(); 189 | t.deepEqual(await stream.toArray(), []); 190 | }); 191 | 192 | test('Can abort the merge stream with no input streams', async t => { 193 | const stream = mergeStreams([]); 194 | stream.destroy(); 195 | await t.throwsAsync(stream.toArray(), prematureClose); 196 | }); 197 | 198 | test('Can destroy the merge stream with no input streams', async t => { 199 | const stream = mergeStreams([]); 200 | const error = new Error('test'); 201 | stream.destroy(error); 202 | t.is(await t.throwsAsync(stream.toArray()), error); 203 | }); 204 | 205 | test('Can emit an "error" event on the merge stream before the input streams', async t => { 206 | const inputStream = Readable.from('.'); 207 | const stream = mergeStreams([inputStream]); 208 | const error = new Error('test'); 209 | stream.emit('error', error); 210 | t.is(await t.throwsAsync(stream.toArray()), error); 211 | t.is(await t.throwsAsync(inputStream.toArray()), error); 212 | }); 213 | 214 | test('Does not end when .unpipe() is called and no stream ended', async t => { 215 | const inputStream = Readable.from('.'); 216 | const stream = mergeStreams([inputStream]); 217 | inputStream.unpipe(stream); 218 | t.true(stream.readable); 219 | t.true(stream.writable); 220 | 221 | stream.end(); 222 | t.is(await text(stream), ''); 223 | }); 224 | 225 | test('Ends when .unpipe() is called and some stream ended', async t => { 226 | const inputStream = Readable.from('.'); 227 | const pendingStream = new PassThrough(); 228 | const stream = mergeStreams([inputStream, pendingStream]); 229 | t.is(await text(inputStream), '.'); 230 | 231 | pendingStream.unpipe(stream); 232 | t.is(await text(stream), '.'); 233 | }); 234 | 235 | test('Aborts when .unpipe() is called and some stream was aborted', async t => { 236 | const inputStream = Readable.from('.'); 237 | const pendingStream = new PassThrough(); 238 | const stream = mergeStreams([inputStream, pendingStream]); 239 | inputStream.destroy(); 240 | 241 | pendingStream.unpipe(stream); 242 | await t.throwsAsync(stream.toArray(), prematureClose); 243 | }); 244 | 245 | test('Errors when .unpipe() is called and some stream errored', async t => { 246 | const inputStream = Readable.from('.'); 247 | const pendingStream = new PassThrough(); 248 | const stream = mergeStreams([inputStream, pendingStream]); 249 | const error = new Error('test'); 250 | inputStream.destroy(error); 251 | 252 | pendingStream.unpipe(stream); 253 | t.is(await t.throwsAsync(stream.toArray()), error); 254 | }); 255 | 256 | test('Does not abort when .unpipe() is called on a different stream', async t => { 257 | const stream = mergeStreams([Readable.from('.')]); 258 | const inputStream = Readable.from(' '); 259 | inputStream.pipe(stream); 260 | inputStream.unpipe(stream); 261 | t.is(await text(stream), '.'); 262 | }); 263 | 264 | test('Keeps piping other streams after one is unpiped', async t => { 265 | const inputStream = Readable.from(' '); 266 | const stream = mergeStreams([inputStream, Readable.from('.')]); 267 | inputStream.unpipe(stream); 268 | t.is(await text(stream), '.'); 269 | }); 270 | 271 | const testListenersCleanup = (t, inputStream, stream) => { 272 | t.is(inputStream.listeners().length, 0); 273 | t.is(stream.listeners().length, 0); 274 | }; 275 | 276 | test('Cleans up input streams listeners on all input streams end', async t => { 277 | const inputStream = Readable.from('.'); 278 | const stream = mergeStreams([inputStream, Readable.from('.')]); 279 | t.is(await text(stream), '..'); 280 | testListenersCleanup(t, inputStream, stream); 281 | }); 282 | 283 | test('Cleans up input streams listeners on any input streams abort', async t => { 284 | const inputStream = Readable.from('.'); 285 | const stream = mergeStreams([inputStream, Readable.from('.')]); 286 | inputStream.destroy(); 287 | t.is(await text(stream), '.'); 288 | testListenersCleanup(t, inputStream, stream); 289 | }); 290 | 291 | test('Cleans up input streams listeners on any input streams error', async t => { 292 | const inputStream = Readable.from('.'); 293 | const stream = mergeStreams([inputStream, Readable.from('.')]); 294 | const error = new Error('test'); 295 | inputStream.destroy(error); 296 | t.is(await t.throwsAsync(stream.toArray()), error); 297 | testListenersCleanup(t, inputStream, stream); 298 | }); 299 | 300 | test('Cleans up input streams listeners on merged stream end', async t => { 301 | const inputStream = getInfiniteStream(); 302 | const stream = mergeStreams([inputStream]); 303 | stream.end(); 304 | await stream.toArray(); 305 | testListenersCleanup(t, inputStream, stream); 306 | }); 307 | 308 | test('Cleans up input streams listeners on merged stream abort', async t => { 309 | const inputStream = getInfiniteStream(); 310 | const stream = mergeStreams([inputStream]); 311 | stream.destroy(); 312 | await t.throwsAsync(stream.toArray(), prematureClose); 313 | testListenersCleanup(t, inputStream, stream); 314 | }); 315 | 316 | test('Cleans up input streams listeners on merged stream error', async t => { 317 | const inputStream = getInfiniteStream(); 318 | const stream = mergeStreams([inputStream]); 319 | const error = new Error('test'); 320 | stream.destroy(error); 321 | t.is(await t.throwsAsync(stream.toArray()), error); 322 | testListenersCleanup(t, inputStream, stream); 323 | }); 324 | 325 | test('The input streams might have already ended', async t => { 326 | const inputStream = Readable.from('.'); 327 | await inputStream.toArray(); 328 | const stream = mergeStreams([inputStream]); 329 | t.deepEqual(await stream.toArray(), []); 330 | }); 331 | 332 | test('The input streams might have already aborted', async t => { 333 | const inputStream = Readable.from('.'); 334 | inputStream.destroy(); 335 | const stream = mergeStreams([inputStream]); 336 | await t.throwsAsync(stream.toArray(), prematureClose); 337 | }); 338 | 339 | test('The input streams might have already errored', async t => { 340 | const inputStream = Readable.from('.'); 341 | const error = new Error('test'); 342 | inputStream.destroy(error); 343 | const stream = mergeStreams([inputStream]); 344 | t.is(await t.throwsAsync(stream.toArray()), error); 345 | }); 346 | 347 | test('The added stream might have already ended', async t => { 348 | const inputStream = Readable.from('.'); 349 | await inputStream.toArray(); 350 | const stream = mergeStreams([Readable.from('.')]); 351 | stream.add(inputStream); 352 | t.is(await text(stream), '.'); 353 | }); 354 | 355 | test('The added stream might have already aborted', async t => { 356 | const inputStream = Readable.from('.'); 357 | inputStream.destroy(); 358 | const stream = mergeStreams([Readable.from('.')]); 359 | stream.add(inputStream); 360 | t.is(await text(stream), '.'); 361 | }); 362 | 363 | test('The added stream might have already errored', async t => { 364 | const inputStream = Readable.from('.'); 365 | const error = new Error('test'); 366 | inputStream.destroy(error); 367 | const stream = mergeStreams([Readable.from('.')]); 368 | stream.add(inputStream); 369 | t.is(await t.throwsAsync(stream.toArray()), error); 370 | }); 371 | 372 | const testHighWaterMarkAmount = async (t, firstObjectMode, secondObjectMode, highWaterMark) => { 373 | const stream = mergeStreams([ 374 | Readable.from(['a', 'b'], {highWaterMark: 4, objectMode: firstObjectMode}), 375 | Readable.from(['c', 'd'], {highWaterMark: 2, objectMode: secondObjectMode}), 376 | ]); 377 | t.is(stream.readableHighWaterMark, highWaterMark); 378 | t.is(stream.writableHighWaterMark, highWaterMark); 379 | await stream.toArray(); 380 | }; 381 | 382 | test('"highWaterMark" is the maximum of non-object input streams', testHighWaterMarkAmount, false, false, 4); 383 | test('"highWaterMark" is the maximum of object input streams', testHighWaterMarkAmount, true, true, 4); 384 | test('"highWaterMark" is the maximum of object streams if mixed with non-object ones', testHighWaterMarkAmount, false, true, 2); 385 | 386 | test('"add()" cannot change highWaterMark', async t => { 387 | const stream = mergeStreams([Readable.from('.', {highWaterMark: 2})]); 388 | stream.add(Readable.from('.', {highWaterMark: 4})); 389 | t.is(stream.readableHighWaterMark, 2); 390 | t.is(stream.writableHighWaterMark, 2); 391 | await stream.toArray(); 392 | }); 393 | 394 | const testBufferSize = async (t, objectMode) => { 395 | const highWaterMark = getDefaultHighWaterMark(objectMode); 396 | const oneStream = new PassThrough({highWaterMark, objectMode}); 397 | const twoStream = new PassThrough({highWaterMark, objectMode}); 398 | const stream = mergeStreams([oneStream, twoStream]); 399 | 400 | // Each PassThrough has a read + write buffer, including the merged stream 401 | // Therefore, there are 6 buffers of size `highWaterMark` 402 | const bufferCount = 6; 403 | 404 | let writeCount = 0; 405 | while (oneStream.write('.') && twoStream.write('.')) { 406 | writeCount += 2; 407 | // eslint-disable-next-line no-await-in-loop 408 | await scheduler.yield(); 409 | } 410 | 411 | // Ensure the maximum amount buffered on writes are those 5 buffers 412 | t.is(writeCount - 2, (highWaterMark - 1) * bufferCount); 413 | 414 | let readCount = 0; 415 | while (stream.read() !== null) { 416 | readCount += 1; 417 | // eslint-disable-next-line no-await-in-loop 418 | await scheduler.yield(); 419 | } 420 | 421 | // When not in object mode, each read retrieves a full buffer, i.e. there are 5 reads 422 | // When in object mode, each read retrieves a single value, i.e. there are as many reads as writes 423 | t.is(readCount, objectMode ? writeCount + 1 : bufferCount); 424 | }; 425 | 426 | test('Use the correct highWaterMark', testBufferSize, false); 427 | test('Use the correct highWaterMark, objectMode', testBufferSize, true); 428 | 429 | test('Buffers streams before consumption', async t => { 430 | const inputStream = Readable.from('.'); 431 | const stream = mergeStreams([inputStream]); 432 | await scheduler.yield(); 433 | 434 | t.is(inputStream.readableLength, 0); 435 | t.false(inputStream.readableFlowing); 436 | t.true(inputStream.destroyed); 437 | 438 | t.is(stream.readableLength, 1); 439 | t.is(stream.readableFlowing, null); 440 | t.false(stream.destroyed); 441 | t.is(await text(stream), '.'); 442 | }); 443 | 444 | const assertMaxListeners = (t, stream, remainingListeners) => { 445 | const listenersMaxCount = Math.max(...stream.eventNames().map(eventName => stream.listenerCount(eventName))); 446 | t.is(stream.getMaxListeners() - listenersMaxCount, remainingListeners); 447 | }; 448 | 449 | test('Does not increment maxListeners of merged streams', async t => { 450 | const length = 1e3; 451 | const inputStreams = Array.from({length}, () => Readable.from(['.'])); 452 | const stream = mergeStreams(inputStreams); 453 | assertMaxListeners(t, stream, defaultMaxListeners); 454 | 455 | await stream.toArray(); 456 | await scheduler.yield(); 457 | t.is(stream.getMaxListeners(), defaultMaxListeners); 458 | }); 459 | 460 | test('Updates maxListeners of merged streams with add() and remove()', async t => { 461 | const stream = mergeStreams([Readable.from('.')]); 462 | assertMaxListeners(t, stream, defaultMaxListeners); 463 | 464 | const inputStream = Readable.from('.'); 465 | stream.add(inputStream); 466 | assertMaxListeners(t, stream, defaultMaxListeners); 467 | 468 | await stream.remove(inputStream); 469 | assertMaxListeners(t, stream, defaultMaxListeners); 470 | 471 | await stream.toArray(); 472 | await scheduler.yield(); 473 | t.is(stream.getMaxListeners(), defaultMaxListeners); 474 | }); 475 | 476 | const testInfiniteMaxListeners = async (t, maxListeners) => { 477 | const stream = mergeStreams([Readable.from('.')]); 478 | stream.setMaxListeners(maxListeners); 479 | t.is(stream.getMaxListeners(), maxListeners); 480 | 481 | stream.add(Readable.from('.')); 482 | t.is(stream.getMaxListeners(), maxListeners); 483 | 484 | await stream.toArray(); 485 | t.is(stream.getMaxListeners(), maxListeners); 486 | }; 487 | 488 | test('Handles setting maxListeners to Infinity', testInfiniteMaxListeners, Number.POSITIVE_INFINITY); 489 | test('Handles setting maxListeners to 0', testInfiniteMaxListeners, 0); 490 | 491 | test('Only increments maxListeners of input streams by 2', async t => { 492 | const inputStream = Readable.from('.'); 493 | inputStream.setMaxListeners(2); 494 | const stream = mergeStreams([inputStream]); 495 | assertMaxListeners(t, inputStream, 0); 496 | await stream.toArray(); 497 | }); 498 | 499 | test('Can add stream after no streams have ended', async t => { 500 | const stream = mergeStreams([Readable.from('.')]); 501 | stream.add(Readable.from('.')); 502 | t.is(await text(stream), '..'); 503 | }); 504 | 505 | test('Can add stream after some streams but not all streams have ended', async t => { 506 | const inputStream = Readable.from('.'); 507 | const pendingStream = new PassThrough(); 508 | const stream = mergeStreams([inputStream, pendingStream]); 509 | t.is(await text(inputStream), '.'); 510 | stream.add(Readable.from('.')); 511 | pendingStream.end('.'); 512 | t.is(await text(stream), '...'); 513 | }); 514 | 515 | test('Can add stream after all streams have ended but it is not used', async t => { 516 | const stream = mergeStreams([Readable.from('.')]); 517 | t.is(await text(stream), '.'); 518 | const pendingStream = new PassThrough(); 519 | stream.add(pendingStream); 520 | t.deepEqual(await stream.toArray(), []); 521 | 522 | t.true(stream.readableEnded); 523 | t.is(stream.errored, null); 524 | t.true(stream.destroyed); 525 | 526 | t.false(pendingStream.readableEnded); 527 | t.is(pendingStream.errored, null); 528 | t.true(pendingStream.destroyed); 529 | }); 530 | 531 | test('Adding same stream twice is a noop', async t => { 532 | const inputStream = Readable.from('.'); 533 | const stream = mergeStreams([inputStream]); 534 | stream.add(inputStream); 535 | t.is(await text(stream), '.'); 536 | }); 537 | 538 | test('Can remove stream before it ends', async t => { 539 | const inputStream = Readable.from('.'); 540 | const stream = mergeStreams([Readable.from('.'), inputStream]); 541 | await stream.remove(inputStream); 542 | t.true(inputStream.readable); 543 | t.is(await text(stream), '.'); 544 | }); 545 | 546 | test('Can remove stream after it ends', async t => { 547 | const inputStream = Readable.from('.'); 548 | const pendingStream = new PassThrough(); 549 | const stream = mergeStreams([pendingStream, inputStream]); 550 | t.is(await text(inputStream), '.'); 551 | await stream.remove(inputStream); 552 | pendingStream.end(' '); 553 | t.is(await text(stream), '. '); 554 | }); 555 | 556 | test('Can remove stream after other streams have ended', async t => { 557 | const inputStream = Readable.from('.'); 558 | const pendingStream = new PassThrough(); 559 | const stream = mergeStreams([pendingStream, inputStream]); 560 | t.is(await text(inputStream), '.'); 561 | await stream.remove(pendingStream); 562 | t.is(await text(stream), '.'); 563 | t.true(pendingStream.readable); 564 | pendingStream.end(); 565 | }); 566 | 567 | test('Can remove stream after other streams have aborted', async t => { 568 | const inputStream = Readable.from('.'); 569 | const pendingStream = new PassThrough(); 570 | const stream = mergeStreams([pendingStream, inputStream]); 571 | inputStream.destroy(); 572 | await stream.remove(pendingStream); 573 | await t.throwsAsync(stream.toArray(), prematureClose); 574 | }); 575 | 576 | test('Can remove stream after other streams have errored', async t => { 577 | const inputStream = Readable.from('.'); 578 | const pendingStream = new PassThrough(); 579 | const stream = mergeStreams([pendingStream, inputStream]); 580 | const error = new Error('test'); 581 | inputStream.destroy(error); 582 | await stream.remove(pendingStream); 583 | t.is(await t.throwsAsync(stream.toArray()), error); 584 | }); 585 | 586 | test('Can remove stream until no input', async t => { 587 | const inputStream = Readable.from('.'); 588 | const stream = mergeStreams([inputStream]); 589 | await stream.remove(inputStream); 590 | t.true(stream.readable); 591 | t.true(stream.writable); 592 | 593 | stream.end(); 594 | t.is(await text(stream), ''); 595 | }); 596 | 597 | test('Can remove then add again a stream', async t => { 598 | const pendingStream = new PassThrough(); 599 | const secondPendingStream = new PassThrough(); 600 | const stream = mergeStreams([pendingStream, secondPendingStream]); 601 | const streamPromise = text(stream); 602 | 603 | secondPendingStream.write('.'); 604 | const firstWrite = await once(stream, 'data'); 605 | t.is(firstWrite.toString(), '.'); 606 | 607 | await stream.remove(secondPendingStream); 608 | 609 | stream.add(secondPendingStream); 610 | pendingStream.end('.'); 611 | const secondWrite = await once(stream, 'data'); 612 | t.is(secondWrite.toString(), '.'); 613 | 614 | secondPendingStream.end('.'); 615 | t.is(await streamPromise, '...'); 616 | }); 617 | 618 | test('Removed streams are not impacted by merge stream end', async t => { 619 | const inputStream = Readable.from('.'); 620 | const pendingStream = new PassThrough(); 621 | const stream = mergeStreams([pendingStream, inputStream]); 622 | await stream.remove(pendingStream); 623 | 624 | t.is(await text(stream), '.'); 625 | 626 | t.true(pendingStream.readable); 627 | pendingStream.end('.'); 628 | t.is(await text(pendingStream), '.'); 629 | }); 630 | 631 | test('Removed streams are not impacted by merge stream abort', async t => { 632 | const inputStream = Readable.from('.'); 633 | const pendingStream = new PassThrough(); 634 | const stream = mergeStreams([pendingStream, inputStream]); 635 | await stream.remove(pendingStream); 636 | 637 | stream.destroy(); 638 | await t.throwsAsync(stream.toArray(), prematureClose); 639 | 640 | t.true(pendingStream.readable); 641 | pendingStream.end('.'); 642 | t.is(await text(pendingStream), '.'); 643 | }); 644 | 645 | test('Removed streams are not impacted by merge stream error', async t => { 646 | const inputStream = Readable.from('.'); 647 | const pendingStream = new PassThrough(); 648 | const stream = mergeStreams([pendingStream, inputStream]); 649 | await stream.remove(pendingStream); 650 | 651 | const error = new Error('test'); 652 | stream.destroy(error); 653 | t.is(await t.throwsAsync(stream.toArray()), error); 654 | 655 | t.true(pendingStream.readable); 656 | pendingStream.end('.'); 657 | t.is(await text(pendingStream), '.'); 658 | }); 659 | 660 | test('remove() returns false when passing the same stream twice', async t => { 661 | const inputStream = Readable.from('.'); 662 | const stream = mergeStreams([inputStream]); 663 | t.true(await stream.remove(inputStream)); 664 | t.false(await stream.remove(inputStream)); 665 | 666 | stream.end(); 667 | await stream.toArray(); 668 | }); 669 | 670 | test('remove() returns false when passing a stream not piped yet', async t => { 671 | const stream = mergeStreams([Readable.from('.')]); 672 | t.false(await stream.remove(Readable.from('.'))); 673 | await stream.toArray(); 674 | }); 675 | 676 | const testInvalidRemove = async (t, removeArgument) => { 677 | const stream = mergeStreams([Readable.from('.')]); 678 | await t.throwsAsync( 679 | stream.remove(removeArgument), 680 | {message: /Expected a readable stream/}, 681 | ); 682 | await stream.toArray(); 683 | }; 684 | 685 | test('remove() throws when passing a non-stream', testInvalidRemove, '.'); 686 | test('remove() throws when passing undefined', testInvalidRemove, undefined); 687 | test('remove() throws when passing null', testInvalidRemove, null); 688 | 689 | test('PassThrough streams methods are not overridden', t => { 690 | t.is(PassThrough.prototype.add, undefined); 691 | t.is(PassThrough.prototype.remove, undefined); 692 | }); 693 | 694 | test('PassThrough streams methods are not enumerable', async t => { 695 | const passThrough = new PassThrough(); 696 | const stream = mergeStreams([Readable.from('.')]); 697 | t.deepEqual(Object.keys(stream).sort(), Object.keys(passThrough).sort()); 698 | await stream.toArray(); 699 | passThrough.end(); 700 | }); 701 | 702 | test('Can use same source stream for multiple merge streams', async t => { 703 | const inputStream = Readable.from('.'); 704 | const stream = mergeStreams([inputStream]); 705 | const streamTwo = mergeStreams([inputStream]); 706 | t.is(await text(stream), '.'); 707 | t.is(await text(streamTwo), '.'); 708 | }); 709 | 710 | test('Can use same source stream for multiple merge streams, with remove()', async t => { 711 | const inputStream = new PassThrough(); 712 | const secondInputStream = Readable.from('.'); 713 | const stream = mergeStreams([inputStream, secondInputStream]); 714 | const secondStream = mergeStreams([inputStream, secondInputStream]); 715 | await stream.remove(inputStream); 716 | t.is(await text(stream), '.'); 717 | inputStream.end('.'); 718 | t.is(await text(secondStream), '..'); 719 | }); 720 | 721 | test('Can call add() right after add()', async t => { 722 | const inputStream = Readable.from('.'); 723 | const secondInputStream = Readable.from('.'); 724 | const stream = mergeStreams([Readable.from('.')]); 725 | stream.add(inputStream); 726 | stream.add(secondInputStream); 727 | t.is(await text(stream), '...'); 728 | }); 729 | 730 | test('Can call add() right after remove()', async t => { 731 | const inputStream = new PassThrough(); 732 | const secondInputStream = Readable.from('.'); 733 | const stream = mergeStreams([inputStream, secondInputStream]); 734 | await stream.remove(inputStream); 735 | stream.add(inputStream); 736 | inputStream.end('.'); 737 | t.is(await text(stream), '..'); 738 | }); 739 | 740 | test('Can call remove() right after add()', async t => { 741 | const inputStream = Readable.from('.'); 742 | const secondInputStream = Readable.from('.'); 743 | const stream = mergeStreams([inputStream]); 744 | stream.add(secondInputStream); 745 | await stream.remove(secondInputStream); 746 | t.is(await text(stream), '.'); 747 | t.is(await text(secondInputStream), '.'); 748 | }); 749 | 750 | test('Can call remove() right after remove()', async t => { 751 | const inputStream = Readable.from('.'); 752 | const secondInputStream = Readable.from('.'); 753 | const stream = mergeStreams([inputStream, secondInputStream, Readable.from('.')]); 754 | await stream.remove(inputStream); 755 | await stream.remove(secondInputStream); 756 | t.is(await text(stream), '.'); 757 | t.is(await text(inputStream), '.'); 758 | t.is(await text(secondInputStream), '.'); 759 | }); 760 | 761 | test('Can call remove() at the same time as remove()', async t => { 762 | const inputStream = Readable.from('.'); 763 | const stream = mergeStreams([inputStream, Readable.from('.')]); 764 | t.deepEqual(await Promise.all([ 765 | stream.remove(inputStream), 766 | stream.remove(inputStream), 767 | ]), [true, false]); 768 | t.is(await text(stream), '.'); 769 | t.is(await text(inputStream), '.'); 770 | }); 771 | --------------------------------------------------------------------------------