├── .gitignore ├── .travis.yml ├── test ├── end-returns-this.js ├── empty-stream-emits-end-without-read.js ├── collect-with-error-end.js ├── readable-only-when-buffering.js ├── auto-end-deferred-when-paused.js ├── iteration-unsupported.js ├── emit-during-end-event.js ├── pipe-ended-stream.js ├── dest-write-returns-nonboolean.js ├── end-missed.js ├── empty-buffer-end-with-encoding.js ├── end-twice.js ├── is-stream.js ├── collect.js ├── array-buffers.js ├── destroy.js ├── iteration.js └── basic.js ├── bench ├── lib │ ├── nullsink.js │ ├── extend-minipass.js │ ├── extend-transform.js │ ├── extend-through2.js │ ├── timer.js │ └── numbers.js └── test.js ├── LICENSE ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | node_modules 3 | .nyc_output/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 12 5 | - 10 6 | -------------------------------------------------------------------------------- /test/end-returns-this.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const MP = require('../') 3 | const mp = new MP() 4 | t.equal(mp.end(), mp, 'end returns this') 5 | -------------------------------------------------------------------------------- /test/empty-stream-emits-end-without-read.js: -------------------------------------------------------------------------------- 1 | const MP = require('../') 2 | const t = require('tap') 3 | t.test('empty end emits end without reading', t => 4 | new MP().end().promise()) 5 | -------------------------------------------------------------------------------- /bench/lib/nullsink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EE = require('events').EventEmitter 3 | 4 | module.exports = class NullSink extends EE { 5 | write (data, encoding, next) { 6 | if (next) next() 7 | return true 8 | } 9 | end () { 10 | this.emit('finish') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bench/lib/extend-minipass.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const MiniPass = require('../..') 3 | 4 | module.exports = class ExtendMiniPass extends MiniPass { 5 | constructor (opts) { 6 | super(opts) 7 | } 8 | write (data, encoding) { 9 | return super.write(data, encoding) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/collect-with-error-end.js: -------------------------------------------------------------------------------- 1 | const MP = require('../') 2 | const mp = new MP() 3 | const poop = new Error('poop') 4 | mp.on('end', () => mp.emit('error', poop)) 5 | mp.end('foo') 6 | const t = require('tap') 7 | t.test('promise catches error emitted on end', t => 8 | t.rejects(mp.collect(), poop)) 9 | -------------------------------------------------------------------------------- /bench/lib/extend-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | module.exports = class ExtendTransform extends stream.Transform { 4 | constructor (opts) { 5 | super(opts) 6 | } 7 | _transform (data, enc, done) { 8 | this.push(data, enc) 9 | done() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bench/lib/extend-through2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const through2 = require('through2') 3 | module.exports = function (opt) { 4 | return opt.objectMode 5 | ? through2.obj(func) 6 | : through2(func) 7 | 8 | function func (data, enc, done) { 9 | this.push(data, enc) 10 | done() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/readable-only-when-buffering.js: -------------------------------------------------------------------------------- 1 | const MP = require('../') 2 | const t = require('tap') 3 | const mp = new MP() 4 | let readables = 0 5 | mp.on('readable', () => readables++) 6 | const ondata = d => {} 7 | mp.on('data', ondata) 8 | t.equal(mp.write('foo'), true) 9 | t.equal(readables, 0) 10 | mp.pause() 11 | t.equal(mp.write('foo'), false) 12 | t.equal(readables, 1) 13 | -------------------------------------------------------------------------------- /test/auto-end-deferred-when-paused.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const MP = require('../') 3 | t.test('do not auto-end empty stream if explicitly paused', t => { 4 | const mp = new MP() 5 | let waitedForEnd = false 6 | mp.pause() 7 | setTimeout(() => { 8 | waitedForEnd = true 9 | mp.resume() 10 | }) 11 | return mp.end().promise().then(() => t.ok(waitedForEnd, 'waited for end')) 12 | }) 13 | -------------------------------------------------------------------------------- /test/iteration-unsupported.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | global._MP_NO_ITERATOR_SYMBOLS_ = '1' 4 | const MP = require('../index.js') 5 | const mp = new MP 6 | mp.write('foo') 7 | setTimeout(() => mp.end()) 8 | t.throws(() => { 9 | for (let x of mp) { 10 | t.fail('should not be iterable') 11 | } 12 | }) 13 | t.rejects(async () => { 14 | for await (let x of mp) { 15 | t.fail('should not be async iterable') 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /bench/lib/timer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = _ => { 3 | const start = process.hrtime() 4 | return _ => { 5 | const end = process.hrtime(start) 6 | const ms = Math.round(end[0]*1e6 + end[1]/1e3)/1e3 7 | if (!process.env.isTTY) 8 | console.log(ms) 9 | else { 10 | const s = Math.round(end[0]*10 + end[1]/1e8)/10 11 | const ss = s <= 1 ? '' : ' (' + s + 's)' 12 | console.log('%d%s', ms, ss) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/emit-during-end-event.js: -------------------------------------------------------------------------------- 1 | const Minipass = require('../') 2 | const t = require('tap') 3 | 4 | class FancyEnder extends Minipass { 5 | emit (ev, ...data) { 6 | if (ev === 'end') 7 | this.emit('foo') 8 | return super.emit(ev, ...data) 9 | } 10 | } 11 | 12 | const mp = new FancyEnder() 13 | let fooEmits = 0 14 | mp.on('foo', () => fooEmits++) 15 | mp.end('asdf') 16 | mp.resume() 17 | t.equal(fooEmits, 1, 'should only see one event emitted') 18 | -------------------------------------------------------------------------------- /test/pipe-ended-stream.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const MP = require('../') 3 | t.test('pipe from ended stream', t => { 4 | const from = new MP() 5 | from.end().on('end', () => { 6 | t.equal(from.emittedEnd, true, 'from already emitted end') 7 | from.pipe(new MP()).on('end', () => t.end()) 8 | }) 9 | }) 10 | 11 | t.test('pipe from ended stream with a promise', t => { 12 | const from = new MP() 13 | return from.end().promise().then(() => 14 | from.pipe(new MP()).promise()) 15 | }) 16 | -------------------------------------------------------------------------------- /test/dest-write-returns-nonboolean.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const MP = require('../') 3 | 4 | t.test('writing to a non-bool returning write() does not pause', t => { 5 | const booleyStream = new (class extends MP { 6 | write (chunk, encoding, cb) { 7 | // no return! 8 | super.write(chunk, encoding, cb) 9 | } 10 | }) 11 | 12 | const booleyStream2 = new (class extends MP { 13 | write (chunk, encoding, cb) { 14 | // no return! 15 | super.write(chunk, encoding, cb) 16 | } 17 | }) 18 | 19 | 20 | const src = new MP 21 | 22 | try { 23 | return src.pipe(booleyStream).pipe(booleyStream2).concat().then(d => 24 | t.equal(d.toString(), 'hello', 'got data all the way through')) 25 | } finally { 26 | src.end('hello') 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) npm, Inc. and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/end-missed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const MP = require('../') 4 | 5 | t.test('end is not missed if listened to after end', t => { 6 | t.plan(1) 7 | const mp = new MP() 8 | mp.end('foo') 9 | let str = '' 10 | mp.on('data', d => str += d) 11 | mp.on('end', () => t.equal(str, 'foo')) 12 | }) 13 | 14 | t.test('listening for any endish event after end re-emits', t => { 15 | t.plan(1) 16 | const mp = new MP() 17 | mp.end('foo') 18 | let str = '' 19 | mp.on('data', d => str += d) 20 | mp.on('finish', () => t.equal(str, 'foo')) 21 | }) 22 | 23 | t.test('all endish listeners get called', t => { 24 | t.plan(3) 25 | const mp = new MP() 26 | let str = '' 27 | mp.on('finish', () => t.equal(str, 'foo')) 28 | mp.on('prefinish', () => t.equal(str, 'foo')) 29 | mp.end('foo') 30 | mp.on('data', d => str += d) 31 | mp.on('end', () => t.equal(str, 'foo')) 32 | }) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minipass", 3 | "version": "3.1.2", 4 | "description": "minimal implementation of a PassThrough stream", 5 | "main": "index.js", 6 | "dependencies": { 7 | "yallist": "^4.0.0" 8 | }, 9 | "devDependencies": { 10 | "end-of-stream": "^1.4.0", 11 | "tap": "^14.6.5", 12 | "through2": "^2.0.3" 13 | }, 14 | "scripts": { 15 | "test": "tap", 16 | "preversion": "npm test", 17 | "postversion": "npm publish --tag=next", 18 | "postpublish": "git push origin --follow-tags" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/isaacs/minipass.git" 23 | }, 24 | "keywords": [ 25 | "passthrough", 26 | "stream" 27 | ], 28 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 29 | "license": "ISC", 30 | "files": [ 31 | "index.js" 32 | ], 33 | "tap": { 34 | "check-coverage": true 35 | }, 36 | "engines": { 37 | "node": ">=8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bench/lib/numbers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | const numbers = new Array(1000).join(',').split(',').map((v, k) => k) 5 | let acc = '' 6 | const strings = numbers.map(n => acc += n) 7 | const bufs = strings.map(s => new Buffer(s)) 8 | const objs = strings.map(s => ({ str: s })) 9 | 10 | module.exports = class Numbers { 11 | constructor (opt) { 12 | this.objectMode = opt.objectMode 13 | this.encoding = opt.encoding 14 | this.ii = 0 15 | this.done = false 16 | } 17 | pipe (dest) { 18 | this.dest = dest 19 | this.go() 20 | return dest 21 | } 22 | 23 | go () { 24 | let flowing = true 25 | while (flowing) { 26 | if (this.ii >= 1000) { 27 | this.dest.end() 28 | this.done = true 29 | flowing = false 30 | } else { 31 | flowing = this.dest.write( 32 | (this.objectMode ? objs 33 | : this.encoding ? strings 34 | : bufs)[this.ii++]) 35 | } 36 | } 37 | 38 | if (!this.done) 39 | this.dest.once('drain', _ => this.go()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/empty-buffer-end-with-encoding.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const MP = require('../') 3 | 4 | const enc = { encoding: 'utf8' } 5 | 6 | t.test('encoding and immediate end', t => 7 | new MP(enc).end().concat().then(s => t.equal(s, ''))) 8 | 9 | t.test('encoding and end with empty string', t => 10 | new MP(enc).end('').concat().then(s => t.equal(s, ''))) 11 | 12 | t.test('encoding and end with empty buffer', t => 13 | new MP(enc).end(Buffer.alloc(0)).concat().then(s => t.equal(s, ''))) 14 | 15 | t.test('encoding and end with stringly empty buffer', t => 16 | new MP(enc).end(Buffer.from('')).concat().then(s => t.equal(s, ''))) 17 | 18 | t.test('encoding and write then end with empty buffer', t => { 19 | const mp = new MP(enc) 20 | mp.write('ok') 21 | return mp.end(Buffer.alloc(0)).concat().then(s => t.equal(s, 'ok')) 22 | }) 23 | 24 | t.test('encoding and write then end with empty string', t => { 25 | const mp = new MP(enc) 26 | mp.write('ok') 27 | return mp.end('').concat().then(s => t.equal(s, 'ok')) 28 | }) 29 | 30 | t.test('empty write with cb', t => new MP(enc).write(Buffer.from(''), t.end)) 31 | -------------------------------------------------------------------------------- /test/end-twice.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const MP = require('../') 4 | 5 | t.test('organic', t => { 6 | const butterfly = Buffer.from([0x61, 0xf0, 0x9f, 0xa6, 0x8b, 0xf0]) 7 | const mp = new MP({ encoding: 'utf8' }) 8 | 9 | let sawEnd = 0 10 | mp.on('end', () => 11 | t.equal(sawEnd++, 0, 'should not have seen the end yet')) 12 | 13 | mp.once('data', () => { 14 | mp.once('data', () => { 15 | mp.once('data', () => mp.end()) 16 | mp.end() 17 | }) 18 | mp.end(butterfly.slice(3)) 19 | }) 20 | mp.end(butterfly.slice(0, 3)) 21 | 22 | t.equal(sawEnd, 1, 'should see end exactly once') 23 | t.end() 24 | }) 25 | 26 | t.test('manufactured', t => { 27 | // *should* already be impossible, but just to be even more 28 | // deliberate, in case that wasn't the only way it could happen 29 | const mp = new MP() 30 | let sawEnd = 0 31 | mp.on('end', () => { 32 | t.equal(sawEnd++, 0, 'should not have seen the end yet') 33 | mp.emit('end') 34 | }) 35 | mp.emit('end') 36 | mp.emit('end') 37 | 38 | t.equal(sawEnd, 1, 'should see end exactly once') 39 | t.end() 40 | }) 41 | -------------------------------------------------------------------------------- /test/is-stream.js: -------------------------------------------------------------------------------- 1 | const MP = require('../') 2 | const EE = require('events') 3 | const t = require('tap') 4 | const Stream = require('stream') 5 | 6 | t.equal(MP.isStream(new MP), true, 'a MiniPass is a stream') 7 | t.equal(MP.isStream(new Stream), true, 'a Stream is a stream') 8 | t.equal((new MP) instanceof Stream, true, 'a MiniPass is a Stream') 9 | const w = new EE() 10 | w.write = () => {} 11 | w.end = () => {} 12 | t.equal(MP.isStream(w), true, 'EE with write() and end() is a stream') 13 | const r = new EE() 14 | r.pipe = () => {} 15 | t.equal(MP.isStream(r), true, 'EE with pipe() is a stream') 16 | t.equal(MP.isStream(new Stream.Readable()), true, 'Stream.Readable() is a stream') 17 | t.equal(MP.isStream(new Stream.Writable()), true, 'Stream.Writable() is a stream') 18 | t.equal(MP.isStream(new Stream.Duplex()), true, 'Stream.Duplex() is a stream') 19 | t.equal(MP.isStream(new Stream.Transform()), true, 'Stream.Transform() is a stream') 20 | t.equal(MP.isStream(new Stream.PassThrough()), true, 'Stream.PassThrough() is a stream') 21 | t.equal(MP.isStream(new (class extends MP {})), true, 'extends MP is a stream') 22 | t.equal(MP.isStream(new EE), false, 'EE without streaminess is not a stream') 23 | t.equal(MP.isStream({ 24 | write(){}, 25 | end(){}, 26 | pipe(){}, 27 | }), false, 'non-EE is not a stream') 28 | t.equal(MP.isStream('hello'), false, 'string is not a stream') 29 | t.equal(MP.isStream(99), false, 'number is not a stream') 30 | t.equal(MP.isStream(() => {}), false, 'function is not a stream') 31 | t.equal(MP.isStream(null), false, 'null is not a stream') 32 | -------------------------------------------------------------------------------- /test/collect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const MP = require('../index.js') 4 | 5 | t.test('basic', async t => { 6 | const mp = new MP() 7 | let i = 5 8 | const interval = setInterval(() => { 9 | if (i --> 0) 10 | mp.write('foo\n') 11 | else { 12 | clearInterval(interval) 13 | mp.end() 14 | } 15 | }) 16 | mp.setEncoding('utf8') 17 | const all = await mp.collect() 18 | t.same(all, ['foo\n','foo\n','foo\n','foo\n','foo\n']) 19 | }) 20 | 21 | t.test('error', async t => { 22 | const mp = new MP() 23 | const poop = new Error('poop') 24 | setTimeout(() => mp.emit('error', poop)) 25 | await t.rejects(mp.collect(), poop) 26 | }) 27 | 28 | t.test('concat strings', async t => { 29 | const mp = new MP({ encoding: 'utf8' }) 30 | mp.write('foo') 31 | mp.write('bar') 32 | mp.write('baz') 33 | mp.end() 34 | await t.resolveMatch(mp.concat(), 'foobarbaz') 35 | }) 36 | t.test('concat buffers', async t => { 37 | const mp = new MP() 38 | mp.write('foo') 39 | mp.write('bar') 40 | mp.write('baz') 41 | mp.end() 42 | await t.resolveMatch(mp.concat(), Buffer.from('foobarbaz')) 43 | }) 44 | 45 | t.test('concat objectMode fails', async t => { 46 | const a = new MP({objectMode: true}) 47 | await t.rejects(a.concat(), new Error('cannot concat in objectMode')) 48 | const b = new MP() 49 | b.write('asdf') 50 | setTimeout(() => b.end({foo:1})) 51 | await t.rejects(b.concat(), new Error('cannot concat in objectMode')) 52 | }) 53 | 54 | t.test('collect does not set bodyLength in objectMode', t => 55 | new MP({objectMode: true}).end({a:1}).collect().then(data => { 56 | t.equal(typeof data.dataLength, 'undefined') 57 | t.deepEqual(data, [{a:1}]) 58 | })) 59 | -------------------------------------------------------------------------------- /test/array-buffers.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const stringToArrayBuffer = s => { 4 | const buf = Buffer.from(s) 5 | const ab = new ArrayBuffer(buf.length) 6 | const ui = new Uint8Array(ab) 7 | for (let i = 0; i < buf.length; i++) { 8 | ui[i] = buf[i] 9 | } 10 | return ab 11 | } 12 | 13 | const MP = require('../') 14 | 15 | const e = { encoding: 'utf8' } 16 | t.test('write array buffer', t => { 17 | const ab = stringToArrayBuffer('hello world') 18 | const mp = new MP(e).end(ab) 19 | t.equal(mp.objectMode, false, 'array buffer does not trigger objectMode') 20 | return mp.concat().then(s => t.equal(s, 'hello world')) 21 | }) 22 | 23 | t.test('write uint8 typed array', t => { 24 | const ab = stringToArrayBuffer('hello world') 25 | const ui = new Uint8Array(ab, 0, 5) 26 | const mp = new MP(e).end(ui) 27 | t.equal(mp.objectMode, false, 'typed array does not trigger objectMode') 28 | return mp.concat().then(s => t.equal(s, 'hello')) 29 | }) 30 | 31 | const { 32 | ArrayBuffer: VMArrayBuffer, 33 | Uint8Array: VMUint8Array, 34 | } = require('vm').runInNewContext('({ArrayBuffer,Uint8Array})') 35 | 36 | const stringToVMArrayBuffer = s => { 37 | const buf = Buffer.from(s) 38 | const ab = new VMArrayBuffer(buf.length) 39 | const ui = new VMUint8Array(ab) 40 | for (let i = 0; i < buf.length; i++) { 41 | ui[i] = buf[i] 42 | } 43 | return ab 44 | } 45 | 46 | t.test('write vm array buffer', t => { 47 | const ab = stringToVMArrayBuffer('hello world') 48 | const mp = new MP(e).end(ab) 49 | t.equal(mp.objectMode, false, 'array buffer does not trigger objectMode') 50 | return mp.concat().then(s => t.equal(s, 'hello world')) 51 | }) 52 | 53 | t.test('write uint8 typed array', t => { 54 | const ab = stringToVMArrayBuffer('hello world') 55 | const ui = new VMUint8Array(ab, 0, 5) 56 | const mp = new MP(e).end(ui) 57 | t.equal(mp.objectMode, false, 'typed array does not trigger objectMode') 58 | return mp.concat().then(s => t.equal(s, 'hello')) 59 | }) 60 | -------------------------------------------------------------------------------- /test/destroy.js: -------------------------------------------------------------------------------- 1 | const MP = require('../') 2 | const t = require('tap') 3 | 4 | t.match(new MP(), { destroy: Function }, 'destroy is implemented') 5 | 6 | { 7 | const mp = new MP() 8 | t.equal(mp.destroy(), mp, 'destroy() returns this') 9 | } 10 | 11 | t.equal(new MP().destroy().destroyed, true, 'destroy() sets .destroyed getter') 12 | 13 | t.test('destroy(er) emits error', t => { 14 | const mp = new MP() 15 | const er = new Error('skoarchhh') 16 | const ret = t.rejects(() => mp.promise(), er) 17 | mp.destroy(er) 18 | return ret 19 | }) 20 | 21 | t.test('calls close if present', t => { 22 | const mp = new MP() 23 | let closeCalled = false 24 | mp.close = () => { 25 | closeCalled = true 26 | setTimeout(() => mp.emit('close')) 27 | } 28 | mp.on('close', () => { 29 | t.equal(closeCalled, true, 'called close') 30 | t.end() 31 | }) 32 | mp.destroy() 33 | }) 34 | 35 | t.test('destroy a second time just emits the error', t => { 36 | const mp = new MP() 37 | mp.destroy() 38 | const er = new Error('skoarchhh') 39 | const ret = t.rejects(() => mp.promise(), er) 40 | mp.destroy(er) 41 | return ret 42 | }) 43 | 44 | t.test('destroy with no error rejects a promise', t => { 45 | const mp = new MP() 46 | const ret = t.rejects(() => mp.promise(), { message: 'stream destroyed' }) 47 | mp.destroy() 48 | return ret 49 | }) 50 | 51 | t.test('destroy with no error second time rejects a promise', t => { 52 | const mp = new MP() 53 | mp.destroy() 54 | const ret = t.rejects(() => mp.promise(), { message: 'stream destroyed' }) 55 | mp.destroy() 56 | return ret 57 | }) 58 | 59 | t.test('emits after destruction are ignored', t => { 60 | const mp = new MP().destroy() 61 | mp.on('foo', () => t.fail('should not emit foo after destroy')) 62 | mp.emit('foo') 63 | t.end() 64 | }) 65 | 66 | t.test('pipe after destroy is a no-op', t => { 67 | const p = new MP() 68 | p.write('foo') 69 | p.destroy() 70 | const q = new MP() 71 | q.on('data', c => t.fail('should not get data, upstream is destroyed')) 72 | p.pipe(q) 73 | t.end() 74 | }) 75 | 76 | t.test('resume after destroy is a no-op', t => { 77 | const p = new MP() 78 | p.pause() 79 | p.on('resume', () => t.fail('should not see resume event after destroy')) 80 | p.destroy() 81 | p.resume() 82 | t.end() 83 | }) 84 | 85 | t.test('read after destroy always returns null', t => { 86 | const p = new MP({ encoding: 'utf8' }) 87 | p.write('hello, ') 88 | p.write('world') 89 | t.equal(p.read(), 'hello, world') 90 | p.write('destroyed!') 91 | p.destroy() 92 | t.equal(p.read(), null) 93 | t.end() 94 | }) 95 | 96 | t.test('write after destroy emits error', t => { 97 | const p = new MP() 98 | p.destroy() 99 | p.on('error', er => { 100 | t.match(er, { 101 | message: 'Cannot call write after a stream was destroyed', 102 | code: 'ERR_STREAM_DESTROYED', 103 | }) 104 | t.end() 105 | }) 106 | p.write('nope') 107 | }) 108 | -------------------------------------------------------------------------------- /bench/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const iterations = +process.env.BENCH_TEST_ITERATION || 100 4 | const testCount = +process.env.BENCH_TEST_COUNT || 20 5 | 6 | const tests = [ 7 | 'baseline', 8 | 'minipass', 9 | 'extend-minipass', 10 | 'through2', 11 | 'extend-through2', 12 | 'passthrough', 13 | 'extend-transform' 14 | ] 15 | 16 | const manyOpts = [ 'many', 'single' ] 17 | const typeOpts = [ 'buffer', 'string', 'object' ] 18 | 19 | const main = () => { 20 | const spawn = require('child_process').spawn 21 | const node = process.execPath 22 | 23 | const results = {} 24 | 25 | const testSet = [] 26 | tests.forEach(t => 27 | manyOpts.forEach(many => 28 | typeOpts.forEach(type => 29 | new Array(testCount).join(',').split(',').forEach(() => 30 | t !== 'baseline' || (many === 'single' && type === 'object') 31 | ? testSet.push([t, many, type]) : null)))) 32 | 33 | let didFirst = false 34 | const mainRunTest = t => { 35 | if (!t) 36 | return afterMain(results) 37 | 38 | const k = t.join('\t') 39 | if (!results[k]) { 40 | results[k] = [] 41 | if (!didFirst) 42 | didFirst = true 43 | else 44 | process.stderr.write('\n') 45 | 46 | process.stderr.write(k + ' #') 47 | } else { 48 | process.stderr.write('#') 49 | } 50 | 51 | const c = spawn(node, [__filename].concat(t), { 52 | stdio: [ 'ignore', 'pipe', 2 ] 53 | }) 54 | let out = '' 55 | c.stdout.on('data', c => out += c) 56 | c.on('close', (code, signal) => { 57 | if (code || signal) 58 | throw new Error('failed: ' + code + ' ' + signal) 59 | results[k].push(+out) 60 | mainRunTest(testSet.shift()) 61 | }) 62 | } 63 | 64 | mainRunTest(testSet.shift()) 65 | } 66 | 67 | const afterMain = results => { 68 | console.log('test\tmany\ttype\tops/s\tmean\tmedian\tmax\tmin' + 69 | '\tstdev\trange\traw') 70 | // get the mean, median, stddev, and range of each test 71 | Object.keys(results).forEach(test => { 72 | const k = results[test].sort((a, b) => a - b) 73 | const min = k[0] 74 | const max = k[ k.length - 1 ] 75 | const range = max - min 76 | const sum = k.reduce((a,b) => a + b, 0) 77 | const mean = sum / k.length 78 | const ops = iterations / mean * 1000 79 | const devs = k.map(n => n - mean).map(n => n * n) 80 | const avgdev = devs.reduce((a,b) => a + b, 0) / k.length 81 | const stdev = Math.pow(avgdev, 0.5) 82 | const median = k.length % 2 ? k[Math.floor(k.length / 2)] : 83 | (k[k.length/2] + k[k.length/2+1])/2 84 | console.log( 85 | '%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%s', test, round(ops), 86 | round(mean), round(median), 87 | max, min, round(stdev), round(range), 88 | k.join('\t')) 89 | }) 90 | } 91 | 92 | const round = num => Math.round(num * 1000)/1000 93 | 94 | const test = (testname, many, type) => { 95 | const timer = require('./lib/timer.js') 96 | const Class = getClass(testname) 97 | 98 | const done = timer() 99 | runTest(Class, many, type, iterations, done) 100 | } 101 | 102 | // don't blow up the stack! loop unless deferred 103 | const runTest = (Class, many, type, iterations, done) => { 104 | const Nullsink = require('./lib/nullsink.js') 105 | const Numbers = require('./lib/numbers.js') 106 | const opt = {} 107 | if (type === 'string') 108 | opt.encoding = 'utf8' 109 | else if (type === 'object') 110 | opt.objectMode = true 111 | 112 | while (iterations--) { 113 | let finished = false 114 | let inloop = true 115 | const after = iterations === 0 ? done 116 | : () => { 117 | if (iterations === 0) 118 | done() 119 | else if (inloop) 120 | finished = true 121 | else 122 | runTest(Class, many, type, iterations, done) 123 | } 124 | 125 | const out = new Nullsink().on('finish', after) 126 | let sink = Class ? new Class(opt) : out 127 | 128 | if (many && Class) 129 | sink = sink 130 | .pipe(new Class(opt)) 131 | .pipe(new Class(opt)) 132 | .pipe(new Class(opt)) 133 | .pipe(new Class(opt)) 134 | 135 | if (sink !== out) 136 | sink.pipe(out) 137 | 138 | new Numbers(opt).pipe(sink) 139 | 140 | // keep tight-looping if the stream is done already 141 | if (!finished) { 142 | inloop = false 143 | break 144 | } 145 | } 146 | } 147 | 148 | const getClass = testname => 149 | testname === 'through2' ? require('through2').obj 150 | : testname === 'extend-through2' ? require('./lib/extend-through2.js') 151 | : testname === 'minipass' ? require('../') 152 | : testname === 'extend-minipass' ? require('./lib/extend-minipass.js') 153 | : testname === 'passthrough' ? require('stream').PassThrough 154 | : testname === 'extend-transform' ? require('./lib/extend-transform.js') 155 | : null 156 | 157 | if (!process.argv[2]) 158 | main() 159 | else 160 | test(process.argv[2], process.argv[3] === 'many', process.argv[4]) 161 | -------------------------------------------------------------------------------- /test/iteration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const MP = require('../index.js') 4 | 5 | t.test('sync iteration', t => { 6 | const cases = { 7 | 'buffer': [ null, [ 8 | Buffer.from('ab'), 9 | Buffer.from('cd'), 10 | Buffer.from('e') 11 | ]], 12 | 'string': [ { encoding: 'utf8' }, ['ab', 'cd', 'e']], 13 | 'object': [ { objectMode: true }, ['a', 'b', 'c', 'd', 'e']] 14 | } 15 | const runTest = (c, opt, expect) => { 16 | t.test(c, t => { 17 | const result = [] 18 | const mp = new MP(opt) 19 | mp.write('a') 20 | mp.write('b') 21 | for (let letter of mp) { 22 | result.push(letter) 23 | } 24 | mp.write('c') 25 | mp.write('d') 26 | result.push.call(result, ...mp) 27 | mp.write('e') 28 | mp.end() 29 | for (let letter of mp) { 30 | result.push(letter) // e 31 | } 32 | for (let letter of mp) { 33 | result.push(letter) // nothing 34 | } 35 | t.match(result, expect) 36 | t.end() 37 | }) 38 | } 39 | 40 | for (let c in cases) { 41 | runTest(c, cases[c][0], cases[c][1]) 42 | } 43 | 44 | t.test('destroy while iterating', t => { 45 | const mp = new MP({ objectMode: true }) 46 | mp.write('a') 47 | mp.write('b') 48 | mp.write('c') 49 | mp.write('d') 50 | const result = [] 51 | for (let letter of mp) { 52 | result.push(letter) 53 | mp.destroy() 54 | } 55 | t.same(result, ['a']) 56 | t.end() 57 | }) 58 | 59 | t.end() 60 | }) 61 | 62 | t.test('async iteration', t => { 63 | const expect = [ 64 | 'start\n', 65 | 'foo\n', 66 | 'foo\n', 67 | 'foo\n', 68 | 'foo\n', 69 | 'foo\n', 70 | 'bar\n' 71 | ] 72 | 73 | t.test('end immediate', async t => { 74 | const mp = new MP({ encoding: 'utf8' }) 75 | let i = 5 76 | 77 | mp.write('start\n') 78 | const inter = setInterval(() => { 79 | if (i --> 0) 80 | mp.write(Buffer.from('foo\n', 'utf8')) 81 | else { 82 | mp.end('bar\n') 83 | clearInterval(inter) 84 | } 85 | }) 86 | 87 | const result = [] 88 | for await (let x of mp) 89 | result.push(x) 90 | 91 | t.same(result, expect) 92 | }) 93 | 94 | t.test('end later', async t => { 95 | const mp = new MP({ encoding: 'utf8' }) 96 | let i = 5 97 | 98 | mp.write('start\n') 99 | const inter = setInterval(() => { 100 | if (i --> 0) 101 | mp.write(Buffer.from('foo\n', 'utf8')) 102 | else { 103 | mp.write('bar\n') 104 | setTimeout(() => mp.end()) 105 | clearInterval(inter) 106 | } 107 | }) 108 | 109 | const result = [] 110 | for await (let x of mp) 111 | result.push(x) 112 | 113 | t.same(result, expect) 114 | }) 115 | 116 | t.test('multiple chunks at once, asyncly', async t => { 117 | const mp = new MP() 118 | let i = 6 119 | const write = () => { 120 | if (i === 6) 121 | mp.write(Buffer.from('start\n', 'utf8')) 122 | else if (i > 0) 123 | mp.write('foo\n') 124 | else if (i === 0) { 125 | mp.end('bar\n') 126 | clearInterval(inter) 127 | } 128 | i-- 129 | } 130 | 131 | const inter = setInterval(() => { 132 | write() 133 | write() 134 | write() 135 | }) 136 | 137 | const result = [] 138 | for await (let x of mp) 139 | result.push(x) 140 | 141 | t.same(result.map(x => x.toString()).join(''), expect.join('')) 142 | }) 143 | 144 | t.test('multiple object chunks at once, asyncly', async t => { 145 | const mp = new MP({ objectMode: true }) 146 | let i = 6 147 | const write = () => { 148 | if (i === 6) 149 | mp.write(['start\n']) 150 | else if (i > 0) 151 | mp.write(['foo\n']) 152 | else if (i === 0) { 153 | mp.end(['bar\n']) 154 | clearInterval(inter) 155 | } 156 | i-- 157 | } 158 | 159 | const inter = setInterval(() => { 160 | write() 161 | write() 162 | write() 163 | }) 164 | 165 | const result = [] 166 | for await (let x of mp) 167 | result.push(x) 168 | 169 | t.same(result.map(x => x.join('')).join(''), expect.join('')) 170 | }) 171 | 172 | t.test('all chunks at once, asyncly', async t => { 173 | const mp = new MP() 174 | setTimeout(() => { 175 | mp.write(Buffer.from('start\n', 'utf8')) 176 | for (let i = 0; i < 5; i++) { 177 | mp.write('foo\n') 178 | } 179 | mp.end('bar\n') 180 | }) 181 | 182 | const result = [] 183 | for await (let x of mp) 184 | result.push(x) 185 | 186 | t.same(result.map(x => x.toString()).join(''), expect.join('')) 187 | }) 188 | 189 | t.test('all object chunks at once, asyncly', async t => { 190 | const mp = new MP({ objectMode: true }) 191 | setTimeout(() => { 192 | mp.write(['start\n']) 193 | for (let i = 0; i < 5; i++) { 194 | mp.write(['foo\n']) 195 | } 196 | mp.end(['bar\n']) 197 | }) 198 | 199 | const result = [] 200 | for await (let x of mp) 201 | result.push(x) 202 | 203 | t.same(result.map(x => x.join('')).join(''), expect.join('')) 204 | }) 205 | 206 | t.test('all object chunks at once, syncly', async t => { 207 | const mp = new MP({ objectMode: true }) 208 | mp.write(['start\n']) 209 | for (let i = 0; i < 5; i++) { 210 | mp.write(['foo\n']) 211 | } 212 | mp.end(['bar\n']) 213 | 214 | const result = [] 215 | for await (let x of mp) 216 | result.push(x) 217 | 218 | t.same(result.map(x => x.join('')).join(''), expect.join('')) 219 | }) 220 | 221 | t.test('pipe in all at once', async t => { 222 | const inp = new MP({ encoding: 'utf8' }) 223 | const mp = new MP({ encoding: 'utf8' }) 224 | inp.pipe(mp) 225 | 226 | let i = 5 227 | inp.write('start\n') 228 | const inter = setInterval(() => { 229 | if (i --> 0) 230 | inp.write(Buffer.from('foo\n', 'utf8')) 231 | else { 232 | inp.end('bar\n') 233 | clearInterval(inter) 234 | } 235 | }) 236 | 237 | const result = [] 238 | for await (let x of mp) 239 | result.push(x) 240 | 241 | t.same(result, expect) 242 | }) 243 | 244 | t.test('pipe in multiple object chunks at once, asyncly', async t => { 245 | const mp = new MP({ objectMode: true }) 246 | const inp = new MP({ objectMode: true }) 247 | inp.pipe(mp) 248 | 249 | let i = 5 250 | inp.write(['start\n']) 251 | const write = () => { 252 | if (i > 0) 253 | inp.write(['foo\n']) 254 | else if (i === 0) { 255 | inp.end(['bar\n']) 256 | clearInterval(inter) 257 | } 258 | i-- 259 | } 260 | 261 | const inter = setInterval(() => { 262 | write() 263 | write() 264 | write() 265 | }) 266 | 267 | const result = [] 268 | for await (let x of mp) 269 | result.push(x) 270 | 271 | t.same(result.map(x => x.join('')).join(''), expect.join('')) 272 | }) 273 | 274 | t.test('throw error', async t => { 275 | const mp = new MP() 276 | const poop = new Error('poop') 277 | setTimeout(() => { 278 | mp.read = () => { throw poop } 279 | mp.end('this is fine') 280 | }) 281 | const result = [] 282 | const run = async () => { 283 | for await (let x of mp) { 284 | result.push(x) 285 | } 286 | } 287 | 288 | await t.rejects(run, poop) 289 | }) 290 | 291 | t.test('emit error', async t => { 292 | const mp = new MP() 293 | const poop = new Error('poop') 294 | setTimeout(() => mp.emit('error', poop)) 295 | const result = [] 296 | const run = async () => { 297 | for await (let x of mp) { 298 | result.push(x) 299 | } 300 | } 301 | 302 | await t.rejects(run, poop) 303 | }) 304 | 305 | t.test('destroy', async t => { 306 | const mp = new MP() 307 | const poop = new Error('poop') 308 | setTimeout(() => mp.destroy()) 309 | const result = [] 310 | const run = async () => { 311 | for await (let x of mp) { 312 | result.push(x) 313 | } 314 | } 315 | 316 | await t.rejects(run, { message: 'stream destroyed' }) 317 | }) 318 | 319 | t.end() 320 | }) 321 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const MiniPass = require('../') 2 | const t = require('tap') 3 | const EE = require('events').EventEmitter 4 | 5 | t.test('some basic piping and writing', async t => { 6 | let mp = new MiniPass({ encoding: 'base64' }) 7 | t.notOk(mp.flowing) 8 | mp.flowing = true 9 | t.notOk(mp.flowing) 10 | t.equal(mp.encoding, 'base64') 11 | mp.encoding = null 12 | t.equal(mp.encoding, null) 13 | t.equal(mp.readable, true) 14 | t.equal(mp.writable, true) 15 | t.equal(mp.write('hello'), false) 16 | let dest = new MiniPass() 17 | let sawDestData = false 18 | dest.once('data', chunk => { 19 | sawDestData = true 20 | t.isa(chunk, Buffer) 21 | }) 22 | t.equal(mp.pipe(dest), dest, 'pipe returns dest') 23 | t.ok(sawDestData, 'got data becasue pipe() flushes') 24 | t.equal(mp.write('bye'), true, 'write() returns true when flowing') 25 | dest.pause() 26 | t.equal(mp.write('after pause'), false, 'false when dest is paused') 27 | t.equal(mp.write('after false'), false, 'false when not flowing') 28 | t.equal(dest.buffer.length, 1, '1 item is buffered in dest') 29 | t.equal(mp.buffer.length, 1, '1 item buffered in src') 30 | dest.resume() 31 | t.equal(dest.buffer.length, 0, 'nothing is buffered in dest') 32 | t.equal(mp.buffer.length, 0, 'nothing buffered in src') 33 | }) 34 | 35 | t.test('unicode splitting', async t => { 36 | const butterfly = '🦋' 37 | const mp = new MiniPass({ encoding: 'utf8' }) 38 | t.plan(2) 39 | t.equal(mp.encoding, 'utf8') 40 | mp.on('data', chunk => { 41 | t.equal(chunk, butterfly) 42 | }) 43 | const butterbuf = Buffer.from([0xf0, 0x9f, 0xa6, 0x8b]) 44 | mp.write(butterbuf.slice(0, 1)) 45 | mp.write(butterbuf.slice(1, 2)) 46 | mp.write(butterbuf.slice(2, 3)) 47 | mp.write(butterbuf.slice(3, 4)) 48 | mp.end() 49 | }) 50 | 51 | t.test('unicode splitting with setEncoding', async t => { 52 | const butterfly = '🦋' 53 | const mp = new MiniPass({ encoding: 'hex' }) 54 | t.plan(4) 55 | t.equal(mp.encoding, 'hex') 56 | mp.setEncoding('hex') 57 | t.equal(mp.encoding, 'hex') 58 | mp.setEncoding('utf8') 59 | t.equal(mp.encoding, 'utf8') 60 | mp.on('data', chunk => { 61 | t.equal(chunk, butterfly) 62 | }) 63 | const butterbuf = Buffer.from([0xf0, 0x9f, 0xa6, 0x8b]) 64 | mp.write(butterbuf.slice(0, 1)) 65 | mp.write(butterbuf.slice(1, 2)) 66 | mp.write(butterbuf.slice(2, 3)) 67 | mp.write(butterbuf.slice(3, 4)) 68 | mp.end() 69 | }) 70 | 71 | t.test('base64 -> utf8 piping', t => { 72 | t.plan(1) 73 | const butterfly = '🦋' 74 | const mp = new MiniPass({ encoding: 'base64' }) 75 | const dest = new MiniPass({ encoding: 'utf8' }) 76 | mp.pipe(dest) 77 | let out = '' 78 | dest.on('data', c => out += c) 79 | dest.on('end', _ => 80 | t.equal(Buffer.from(out, 'base64').toString('utf8'), butterfly)) 81 | mp.write(butterfly) 82 | mp.end() 83 | }) 84 | 85 | t.test('utf8 -> base64 piping', t => { 86 | t.plan(1) 87 | const butterfly = '🦋' 88 | const mp = new MiniPass({ encoding: 'utf8' }) 89 | const dest = new MiniPass({ encoding: 'base64' }) 90 | mp.pipe(dest) 91 | let out = '' 92 | dest.on('data', c => out += c) 93 | dest.on('end', _ => 94 | t.equal(Buffer.from(out, 'base64').toString('utf8'), butterfly)) 95 | mp.write(butterfly) 96 | mp.end() 97 | }) 98 | 99 | t.test('read method', async t => { 100 | const butterfly = '🦋' 101 | const mp = new MiniPass({ encoding: 'utf8' }) 102 | mp.on('data', c => t.equal(c, butterfly)) 103 | mp.pause() 104 | t.equal(mp.paused, true, 'paused=true') 105 | mp.write(Buffer.from(butterfly)) 106 | t.equal(mp.read(5), null) 107 | t.equal(mp.read(0), null) 108 | t.same(mp.read(2), butterfly) 109 | }) 110 | 111 | t.test('read with no args', async t => { 112 | t.test('buffer -> string', async t => { 113 | const butterfly = '🦋' 114 | const mp = new MiniPass({ encoding: 'utf8' }) 115 | mp.on('data', c => t.equal(c, butterfly)) 116 | mp.pause() 117 | const butterbuf = Buffer.from(butterfly) 118 | mp.write(butterbuf.slice(0, 2)) 119 | mp.write(butterbuf.slice(2)) 120 | t.same(mp.read(), butterfly) 121 | t.equal(mp.read(), null) 122 | }) 123 | 124 | t.test('buffer -> buffer', async t => { 125 | const butterfly = Buffer.from('🦋') 126 | const mp = new MiniPass() 127 | mp.on('data', c => t.same(c, butterfly)) 128 | mp.pause() 129 | mp.write(butterfly.slice(0, 2)) 130 | mp.write(butterfly.slice(2)) 131 | t.same(mp.read(), butterfly) 132 | t.equal(mp.read(), null) 133 | }) 134 | 135 | t.test('string -> buffer', async t => { 136 | const butterfly = '🦋' 137 | const butterbuf = Buffer.from(butterfly) 138 | const mp = new MiniPass() 139 | mp.on('data', c => t.same(c, butterbuf)) 140 | mp.pause() 141 | mp.write(butterfly) 142 | t.same(mp.read(), butterbuf) 143 | t.equal(mp.read(), null) 144 | }) 145 | 146 | t.test('string -> string', async t => { 147 | const butterfly = '🦋' 148 | const mp = new MiniPass({ encoding: 'utf8' }) 149 | mp.on('data', c => t.equal(c, butterfly)) 150 | mp.pause() 151 | mp.write(butterfly[0]) 152 | mp.write(butterfly[1]) 153 | t.same(mp.read(), butterfly) 154 | t.equal(mp.read(), null) 155 | }) 156 | }) 157 | 158 | t.test('partial read', async t => { 159 | const butterfly = '🦋' 160 | const mp = new MiniPass() 161 | const butterbuf = Buffer.from(butterfly) 162 | mp.write(butterbuf.slice(0, 1)) 163 | mp.write(butterbuf.slice(1, 2)) 164 | mp.write(butterbuf.slice(2, 3)) 165 | mp.write(butterbuf.slice(3, 4)) 166 | t.equal(mp.read(5), null) 167 | t.equal(mp.read(0), null) 168 | t.same(mp.read(2), butterbuf.slice(0, 2)) 169 | t.same(mp.read(2), butterbuf.slice(2, 4)) 170 | }) 171 | 172 | t.test('write after end', async t => { 173 | const mp = new MiniPass() 174 | let sawEnd = false 175 | mp.on('end', _ => sawEnd = true) 176 | mp.end('not empty') 177 | t.throws(_ => mp.write('nope')) 178 | t.notOk(sawEnd, 'should not get end event yet (not flowing)') 179 | mp.resume() 180 | t.equal(mp.paused, false, 'paused=false after resume') 181 | t.ok(sawEnd, 'should get end event after resume()') 182 | }) 183 | 184 | t.test('write after end', async t => { 185 | const mp = new MiniPass() 186 | let sawEnd = 0 187 | mp.on('end', _ => sawEnd++) 188 | mp.end() // empty 189 | t.ok(mp.emittedEnd, 'emitted end event') 190 | t.throws(_ => mp.write('nope')) 191 | t.equal(sawEnd, 1, 'should get end event (empty stream)') 192 | mp.resume() 193 | t.ok(sawEnd, 2, 'should get end event again, I guess?') 194 | }) 195 | 196 | t.test('write cb', async t => { 197 | const mp = new MiniPass() 198 | let calledCb = false 199 | mp.write('ok', () => calledCb = true) 200 | t.ok(calledCb) 201 | }) 202 | 203 | t.test('end with chunk', async t => { 204 | let out = '' 205 | const mp = new MiniPass({ encoding: 'utf8' }) 206 | let sawEnd = false 207 | mp.prependListener('end', _ => sawEnd = true) 208 | mp.addListener('data', c => out += c) 209 | let endCb = false 210 | mp.end('ok', _ => endCb = true) 211 | t.equal(out, 'ok') 212 | t.ok(sawEnd, 'should see end event') 213 | t.ok(endCb, 'end cb should get called') 214 | }) 215 | 216 | t.test('no drain if could not entirely drain on resume', async t => { 217 | const mp = new MiniPass() 218 | const dest = new MiniPass({ encoding: 'buffer' }) 219 | t.equal(mp.write('foo'), false) 220 | t.equal(mp.write('bar'), false) 221 | t.equal(mp.write('baz'), false) 222 | t.equal(mp.write('qux'), false) 223 | mp.on('drain', _ => t.fail('should not drain')) 224 | mp.pipe(dest) 225 | }) 226 | 227 | t.test('end with chunk pending', async t => { 228 | const mp = new MiniPass() 229 | t.equal(mp.write('foo'), false) 230 | t.equal(mp.write('626172', 'hex'), false) 231 | t.equal(mp.write('baz'), false) 232 | t.equal(mp.write('qux'), false) 233 | let sawEnd = false 234 | mp.on('end', _ => sawEnd = true) 235 | let endCb = false 236 | mp.end(_ => endCb = true) 237 | t.notOk(endCb, 'endcb should not happen yet') 238 | t.notOk(sawEnd, 'should not see end yet') 239 | let out = '' 240 | mp.on('data', c => out += c) 241 | t.ok(sawEnd, 'see end after flush') 242 | t.ok(endCb, 'end cb after flush') 243 | t.equal(out, 'foobarbazqux') 244 | }) 245 | 246 | t.test('pipe to stderr does not throw', t => { 247 | const spawn = require('child_process').spawn 248 | const module = JSON.stringify(require.resolve('../')) 249 | const fs = require('fs') 250 | const file = __dirname + '/prog.js' 251 | fs.writeFileSync(file, ` 252 | const MP = require(${module}) 253 | const mp = new MP() 254 | mp.pipe(process.stderr) 255 | mp.end("hello") 256 | `) 257 | let err = '' 258 | return new Promise(res => { 259 | const child = spawn(process.execPath, [file]) 260 | child.stderr.on('data', c => err += c) 261 | child.on('close', (code, signal) => { 262 | t.equal(code, 0) 263 | t.equal(signal, null) 264 | t.equal(err, 'hello') 265 | fs.unlinkSync(file) 266 | res() 267 | }) 268 | }) 269 | }) 270 | 271 | t.test('emit works with many args', t => { 272 | const mp = new MiniPass() 273 | t.plan(2) 274 | mp.on('foo', function (a, b, c, d, e, f, g) { 275 | t.same([a,b,c,d,e,f,g], [1,2,3,4,5,6,7]) 276 | t.equal(arguments.length, 7) 277 | }) 278 | mp.emit('foo', 1, 2, 3, 4, 5, 6, 7) 279 | }) 280 | 281 | t.test('emit drain on resume, even if no flush', t => { 282 | const mp = new MiniPass() 283 | mp.encoding = 'utf8' 284 | 285 | const chunks = [] 286 | class SlowStream extends EE { 287 | write (chunk) { 288 | chunks.push(chunk) 289 | setTimeout(_ => this.emit('drain')) 290 | return false 291 | } 292 | end () { return this.write() } 293 | } 294 | 295 | const ss = new SlowStream() 296 | 297 | mp.pipe(ss) 298 | t.ok(mp.flowing, 'flowing, because piped') 299 | t.equal(mp.write('foo'), false, 'write() returns false, backpressure') 300 | t.equal(mp.buffer.length, 0, 'buffer len is 0') 301 | t.equal(mp.flowing, false, 'flowing false, awaiting drain') 302 | t.same(chunks, ['foo'], 'chunk made it through') 303 | mp.once('drain', _ => { 304 | t.pass('received mp drain event') 305 | t.end() 306 | }) 307 | }) 308 | 309 | t.test('save close for end', t => { 310 | const mp = new MiniPass() 311 | let ended = false 312 | mp.on('close', _ => { 313 | t.equal(ended, true, 'end before close') 314 | t.end() 315 | }) 316 | mp.on('end', _ => { 317 | t.equal(ended, false, 'only end once') 318 | ended = true 319 | }) 320 | 321 | mp.emit('close') 322 | mp.end('foo') 323 | t.equal(ended, false, 'no end until flushed') 324 | mp.resume() 325 | }) 326 | 327 | t.test('eos works', t => { 328 | const eos = require('end-of-stream') 329 | const mp = new MiniPass() 330 | 331 | eos(mp, er => { 332 | if (er) 333 | throw er 334 | t.end() 335 | }) 336 | 337 | mp.emit('close') 338 | mp.end('foo') 339 | mp.resume() 340 | }) 341 | 342 | t.test('bufferLength property', t => { 343 | const eos = require('end-of-stream') 344 | const mp = new MiniPass() 345 | mp.write('a') 346 | mp.write('a') 347 | mp.write('a') 348 | mp.write('a') 349 | mp.write('a') 350 | mp.write('a') 351 | 352 | t.equal(mp.bufferLength, 6) 353 | t.equal(mp.read(7), null) 354 | t.equal(mp.read(3).toString(), 'aaa') 355 | t.equal(mp.bufferLength, 3) 356 | t.equal(mp.read().toString(), 'aaa') 357 | t.equal(mp.bufferLength, 0) 358 | t.end() 359 | }) 360 | 361 | t.test('emit resume event on resume', t => { 362 | const mp = new MiniPass() 363 | t.plan(3) 364 | mp.on('resume', _ => t.pass('got resume event')) 365 | mp.end('asdf') 366 | t.equal(mp.flowing, false, 'not flowing yet') 367 | mp.resume() 368 | t.equal(mp.flowing, true, 'flowing after resume') 369 | }) 370 | 371 | t.test('objectMode', t => { 372 | const mp = new MiniPass({ objectMode: true }) 373 | t.equal(mp.objectMode, true, 'objectMode getter returns value') 374 | mp.objectMode = false 375 | t.equal(mp.objectMode, true, 'objectMode getter is read-only') 376 | const a = { a: 1 } 377 | const b = { b: 1 } 378 | const out = [] 379 | mp.on('data', c => out.push(c)) 380 | mp.on('end', _ => { 381 | t.equal(out.length, 2) 382 | t.equal(out[0], a) 383 | t.equal(out[1], b) 384 | t.same(out, [ { a: 1 }, { b: 1 } ], 'objs not munged') 385 | t.end() 386 | }) 387 | t.ok(mp.write(a)) 388 | t.ok(mp.write(b)) 389 | mp.end() 390 | }) 391 | 392 | t.test('objectMode no encoding', t => { 393 | const mp = new MiniPass({ 394 | objectMode: true, 395 | encoding: 'utf8' 396 | }) 397 | t.equal(mp.encoding, null) 398 | const a = { a: 1 } 399 | const b = { b: 1 } 400 | const out = [] 401 | mp.on('data', c => out.push(c)) 402 | mp.on('end', _ => { 403 | t.equal(out.length, 2) 404 | t.equal(out[0], a) 405 | t.equal(out[1], b) 406 | t.same(out, [ { a: 1 }, { b: 1 } ], 'objs not munged') 407 | t.end() 408 | }) 409 | t.ok(mp.write(a)) 410 | t.ok(mp.write(b)) 411 | mp.end() 412 | }) 413 | 414 | t.test('objectMode read() and buffering', t => { 415 | const mp = new MiniPass({ objectMode: true }) 416 | const a = { a: 1 } 417 | const b = { b: 1 } 418 | t.notOk(mp.write(a)) 419 | t.notOk(mp.write(b)) 420 | t.equal(mp.read(2), a) 421 | t.equal(mp.read(), b) 422 | t.end() 423 | }) 424 | 425 | t.test('set encoding in object mode throws', async t => 426 | t.throws(_ => new MiniPass({ objectMode: true }).encoding = 'utf8', 427 | new Error('cannot set encoding in objectMode'))) 428 | 429 | t.test('set encoding again throws', async t => 430 | t.throws(_ => { 431 | const mp = new MiniPass({ encoding: 'hex' }) 432 | mp.write('ok') 433 | mp.encoding = 'utf8' 434 | }, new Error('cannot change encoding'))) 435 | 436 | t.test('set encoding with existing buffer', async t => { 437 | const mp = new MiniPass() 438 | const butterfly = '🦋' 439 | const butterbuf = Buffer.from(butterfly) 440 | mp.write(butterbuf.slice(0, 1)) 441 | mp.write(butterbuf.slice(1, 2)) 442 | mp.setEncoding('utf8') 443 | mp.write(butterbuf.slice(2)) 444 | t.equal(mp.read(), butterfly) 445 | }) 446 | 447 | t.test('end:false', async t => { 448 | t.plan(1) 449 | const mp = new MiniPass({ encoding: 'utf8' }) 450 | const d = new MiniPass({ encoding: 'utf8' }) 451 | d.end = () => t.threw(new Error('no end no exit no way out')) 452 | d.on('data', c => t.equal(c, 'this is fine')) 453 | mp.pipe(d, { end: false }) 454 | mp.end('this is fine') 455 | }) 456 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EE = require('events') 3 | const Stream = require('stream') 4 | const Yallist = require('yallist') 5 | const SD = require('string_decoder').StringDecoder 6 | 7 | const EOF = Symbol('EOF') 8 | const MAYBE_EMIT_END = Symbol('maybeEmitEnd') 9 | const EMITTED_END = Symbol('emittedEnd') 10 | const EMITTING_END = Symbol('emittingEnd') 11 | const CLOSED = Symbol('closed') 12 | const READ = Symbol('read') 13 | const FLUSH = Symbol('flush') 14 | const FLUSHCHUNK = Symbol('flushChunk') 15 | const ENCODING = Symbol('encoding') 16 | const DECODER = Symbol('decoder') 17 | const FLOWING = Symbol('flowing') 18 | const PAUSED = Symbol('paused') 19 | const RESUME = Symbol('resume') 20 | const BUFFERLENGTH = Symbol('bufferLength') 21 | const BUFFERPUSH = Symbol('bufferPush') 22 | const BUFFERSHIFT = Symbol('bufferShift') 23 | const OBJECTMODE = Symbol('objectMode') 24 | const DESTROYED = Symbol('destroyed') 25 | 26 | // TODO remove when Node v8 support drops 27 | const doIter = global._MP_NO_ITERATOR_SYMBOLS_ !== '1' 28 | const ASYNCITERATOR = doIter && Symbol.asyncIterator 29 | || Symbol('asyncIterator not implemented') 30 | const ITERATOR = doIter && Symbol.iterator 31 | || Symbol('iterator not implemented') 32 | 33 | // events that mean 'the stream is over' 34 | // these are treated specially, and re-emitted 35 | // if they are listened for after emitting. 36 | const isEndish = ev => 37 | ev === 'end' || 38 | ev === 'finish' || 39 | ev === 'prefinish' 40 | 41 | const isArrayBuffer = b => b instanceof ArrayBuffer || 42 | typeof b === 'object' && 43 | b.constructor && 44 | b.constructor.name === 'ArrayBuffer' && 45 | b.byteLength >= 0 46 | 47 | const isArrayBufferView = b => !Buffer.isBuffer(b) && ArrayBuffer.isView(b) 48 | 49 | module.exports = class Minipass extends Stream { 50 | constructor (options) { 51 | super() 52 | this[FLOWING] = false 53 | // whether we're explicitly paused 54 | this[PAUSED] = false 55 | this.pipes = new Yallist() 56 | this.buffer = new Yallist() 57 | this[OBJECTMODE] = options && options.objectMode || false 58 | if (this[OBJECTMODE]) 59 | this[ENCODING] = null 60 | else 61 | this[ENCODING] = options && options.encoding || null 62 | if (this[ENCODING] === 'buffer') 63 | this[ENCODING] = null 64 | this[DECODER] = this[ENCODING] ? new SD(this[ENCODING]) : null 65 | this[EOF] = false 66 | this[EMITTED_END] = false 67 | this[EMITTING_END] = false 68 | this[CLOSED] = false 69 | this.writable = true 70 | this.readable = true 71 | this[BUFFERLENGTH] = 0 72 | this[DESTROYED] = false 73 | } 74 | 75 | get bufferLength () { return this[BUFFERLENGTH] } 76 | 77 | get encoding () { return this[ENCODING] } 78 | set encoding (enc) { 79 | if (this[OBJECTMODE]) 80 | throw new Error('cannot set encoding in objectMode') 81 | 82 | if (this[ENCODING] && enc !== this[ENCODING] && 83 | (this[DECODER] && this[DECODER].lastNeed || this[BUFFERLENGTH])) 84 | throw new Error('cannot change encoding') 85 | 86 | if (this[ENCODING] !== enc) { 87 | this[DECODER] = enc ? new SD(enc) : null 88 | if (this.buffer.length) 89 | this.buffer = this.buffer.map(chunk => this[DECODER].write(chunk)) 90 | } 91 | 92 | this[ENCODING] = enc 93 | } 94 | 95 | setEncoding (enc) { 96 | this.encoding = enc 97 | } 98 | 99 | get objectMode () { return this[OBJECTMODE] } 100 | set objectMode (om) { this[OBJECTMODE] = this[OBJECTMODE] || !!om } 101 | 102 | write (chunk, encoding, cb) { 103 | if (this[EOF]) 104 | throw new Error('write after end') 105 | 106 | if (this[DESTROYED]) { 107 | this.emit('error', Object.assign( 108 | new Error('Cannot call write after a stream was destroyed'), 109 | { code: 'ERR_STREAM_DESTROYED' } 110 | )) 111 | return true 112 | } 113 | 114 | if (typeof encoding === 'function') 115 | cb = encoding, encoding = 'utf8' 116 | 117 | if (!encoding) 118 | encoding = 'utf8' 119 | 120 | // convert array buffers and typed array views into buffers 121 | // at some point in the future, we may want to do the opposite! 122 | // leave strings and buffers as-is 123 | // anything else switches us into object mode 124 | if (!this[OBJECTMODE] && !Buffer.isBuffer(chunk)) { 125 | if (isArrayBufferView(chunk)) 126 | chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) 127 | else if (isArrayBuffer(chunk)) 128 | chunk = Buffer.from(chunk) 129 | else if (typeof chunk !== 'string') 130 | // use the setter so we throw if we have encoding set 131 | this.objectMode = true 132 | } 133 | 134 | // this ensures at this point that the chunk is a buffer or string 135 | // don't buffer it up or send it to the decoder 136 | if (!this.objectMode && !chunk.length) { 137 | const ret = this.flowing 138 | if (this[BUFFERLENGTH] !== 0) 139 | this.emit('readable') 140 | if (cb) 141 | cb() 142 | return ret 143 | } 144 | 145 | // fast-path writing strings of same encoding to a stream with 146 | // an empty buffer, skipping the buffer/decoder dance 147 | if (typeof chunk === 'string' && !this[OBJECTMODE] && 148 | // unless it is a string already ready for us to use 149 | !(encoding === this[ENCODING] && !this[DECODER].lastNeed)) { 150 | chunk = Buffer.from(chunk, encoding) 151 | } 152 | 153 | if (Buffer.isBuffer(chunk) && this[ENCODING]) 154 | chunk = this[DECODER].write(chunk) 155 | 156 | try { 157 | return this.flowing 158 | ? (this.emit('data', chunk), this.flowing) 159 | : (this[BUFFERPUSH](chunk), false) 160 | } finally { 161 | if (this[BUFFERLENGTH] !== 0) 162 | this.emit('readable') 163 | if (cb) 164 | cb() 165 | } 166 | } 167 | 168 | read (n) { 169 | if (this[DESTROYED]) 170 | return null 171 | 172 | try { 173 | if (this[BUFFERLENGTH] === 0 || n === 0 || n > this[BUFFERLENGTH]) 174 | return null 175 | 176 | if (this[OBJECTMODE]) 177 | n = null 178 | 179 | if (this.buffer.length > 1 && !this[OBJECTMODE]) { 180 | if (this.encoding) 181 | this.buffer = new Yallist([ 182 | Array.from(this.buffer).join('') 183 | ]) 184 | else 185 | this.buffer = new Yallist([ 186 | Buffer.concat(Array.from(this.buffer), this[BUFFERLENGTH]) 187 | ]) 188 | } 189 | 190 | return this[READ](n || null, this.buffer.head.value) 191 | } finally { 192 | this[MAYBE_EMIT_END]() 193 | } 194 | } 195 | 196 | [READ] (n, chunk) { 197 | if (n === chunk.length || n === null) 198 | this[BUFFERSHIFT]() 199 | else { 200 | this.buffer.head.value = chunk.slice(n) 201 | chunk = chunk.slice(0, n) 202 | this[BUFFERLENGTH] -= n 203 | } 204 | 205 | this.emit('data', chunk) 206 | 207 | if (!this.buffer.length && !this[EOF]) 208 | this.emit('drain') 209 | 210 | return chunk 211 | } 212 | 213 | end (chunk, encoding, cb) { 214 | if (typeof chunk === 'function') 215 | cb = chunk, chunk = null 216 | if (typeof encoding === 'function') 217 | cb = encoding, encoding = 'utf8' 218 | if (chunk) 219 | this.write(chunk, encoding) 220 | if (cb) 221 | this.once('end', cb) 222 | this[EOF] = true 223 | this.writable = false 224 | 225 | // if we haven't written anything, then go ahead and emit, 226 | // even if we're not reading. 227 | // we'll re-emit if a new 'end' listener is added anyway. 228 | // This makes MP more suitable to write-only use cases. 229 | if (this.flowing || !this[PAUSED]) 230 | this[MAYBE_EMIT_END]() 231 | return this 232 | } 233 | 234 | // don't let the internal resume be overwritten 235 | [RESUME] () { 236 | if (this[DESTROYED]) 237 | return 238 | 239 | this[PAUSED] = false 240 | this[FLOWING] = true 241 | this.emit('resume') 242 | if (this.buffer.length) 243 | this[FLUSH]() 244 | else if (this[EOF]) 245 | this[MAYBE_EMIT_END]() 246 | else 247 | this.emit('drain') 248 | } 249 | 250 | resume () { 251 | return this[RESUME]() 252 | } 253 | 254 | pause () { 255 | this[FLOWING] = false 256 | this[PAUSED] = true 257 | } 258 | 259 | get destroyed () { 260 | return this[DESTROYED] 261 | } 262 | 263 | get flowing () { 264 | return this[FLOWING] 265 | } 266 | 267 | get paused () { 268 | return this[PAUSED] 269 | } 270 | 271 | [BUFFERPUSH] (chunk) { 272 | if (this[OBJECTMODE]) 273 | this[BUFFERLENGTH] += 1 274 | else 275 | this[BUFFERLENGTH] += chunk.length 276 | return this.buffer.push(chunk) 277 | } 278 | 279 | [BUFFERSHIFT] () { 280 | if (this.buffer.length) { 281 | if (this[OBJECTMODE]) 282 | this[BUFFERLENGTH] -= 1 283 | else 284 | this[BUFFERLENGTH] -= this.buffer.head.value.length 285 | } 286 | return this.buffer.shift() 287 | } 288 | 289 | [FLUSH] () { 290 | do {} while (this[FLUSHCHUNK](this[BUFFERSHIFT]())) 291 | 292 | if (!this.buffer.length && !this[EOF]) 293 | this.emit('drain') 294 | } 295 | 296 | [FLUSHCHUNK] (chunk) { 297 | return chunk ? (this.emit('data', chunk), this.flowing) : false 298 | } 299 | 300 | pipe (dest, opts) { 301 | if (this[DESTROYED]) 302 | return 303 | 304 | const ended = this[EMITTED_END] 305 | opts = opts || {} 306 | if (dest === process.stdout || dest === process.stderr) 307 | opts.end = false 308 | else 309 | opts.end = opts.end !== false 310 | 311 | const p = { dest: dest, opts: opts, ondrain: _ => this[RESUME]() } 312 | this.pipes.push(p) 313 | 314 | dest.on('drain', p.ondrain) 315 | this[RESUME]() 316 | // piping an ended stream ends immediately 317 | if (ended && p.opts.end) 318 | p.dest.end() 319 | return dest 320 | } 321 | 322 | addListener (ev, fn) { 323 | return this.on(ev, fn) 324 | } 325 | 326 | on (ev, fn) { 327 | try { 328 | return super.on(ev, fn) 329 | } finally { 330 | if (ev === 'data' && !this.pipes.length && !this.flowing) 331 | this[RESUME]() 332 | else if (isEndish(ev) && this[EMITTED_END]) { 333 | super.emit(ev) 334 | this.removeAllListeners(ev) 335 | } 336 | } 337 | } 338 | 339 | get emittedEnd () { 340 | return this[EMITTED_END] 341 | } 342 | 343 | [MAYBE_EMIT_END] () { 344 | if (!this[EMITTING_END] && 345 | !this[EMITTED_END] && 346 | !this[DESTROYED] && 347 | this.buffer.length === 0 && 348 | this[EOF]) { 349 | this[EMITTING_END] = true 350 | this.emit('end') 351 | this.emit('prefinish') 352 | this.emit('finish') 353 | if (this[CLOSED]) 354 | this.emit('close') 355 | this[EMITTING_END] = false 356 | } 357 | } 358 | 359 | emit (ev, data) { 360 | // error and close are only events allowed after calling destroy() 361 | if (ev !== 'error' && ev !== 'close' && ev !== DESTROYED && this[DESTROYED]) 362 | return 363 | else if (ev === 'data') { 364 | if (!data) 365 | return 366 | 367 | if (this.pipes.length) 368 | this.pipes.forEach(p => 369 | p.dest.write(data) === false && this.pause()) 370 | } else if (ev === 'end') { 371 | // only actual end gets this treatment 372 | if (this[EMITTED_END] === true) 373 | return 374 | 375 | this[EMITTED_END] = true 376 | this.readable = false 377 | 378 | if (this[DECODER]) { 379 | data = this[DECODER].end() 380 | if (data) { 381 | this.pipes.forEach(p => p.dest.write(data)) 382 | super.emit('data', data) 383 | } 384 | } 385 | 386 | this.pipes.forEach(p => { 387 | p.dest.removeListener('drain', p.ondrain) 388 | if (p.opts.end) 389 | p.dest.end() 390 | }) 391 | } else if (ev === 'close') { 392 | this[CLOSED] = true 393 | // don't emit close before 'end' and 'finish' 394 | if (!this[EMITTED_END] && !this[DESTROYED]) 395 | return 396 | } 397 | 398 | // TODO: replace with a spread operator when Node v4 support drops 399 | const args = new Array(arguments.length) 400 | args[0] = ev 401 | args[1] = data 402 | if (arguments.length > 2) { 403 | for (let i = 2; i < arguments.length; i++) { 404 | args[i] = arguments[i] 405 | } 406 | } 407 | 408 | try { 409 | return super.emit.apply(this, args) 410 | } finally { 411 | if (!isEndish(ev)) 412 | this[MAYBE_EMIT_END]() 413 | else 414 | this.removeAllListeners(ev) 415 | } 416 | } 417 | 418 | // const all = await stream.collect() 419 | collect () { 420 | const buf = [] 421 | if (!this[OBJECTMODE]) 422 | buf.dataLength = 0 423 | // set the promise first, in case an error is raised 424 | // by triggering the flow here. 425 | const p = this.promise() 426 | this.on('data', c => { 427 | buf.push(c) 428 | if (!this[OBJECTMODE]) 429 | buf.dataLength += c.length 430 | }) 431 | return p.then(() => buf) 432 | } 433 | 434 | // const data = await stream.concat() 435 | concat () { 436 | return this[OBJECTMODE] 437 | ? Promise.reject(new Error('cannot concat in objectMode')) 438 | : this.collect().then(buf => 439 | this[OBJECTMODE] 440 | ? Promise.reject(new Error('cannot concat in objectMode')) 441 | : this[ENCODING] ? buf.join('') : Buffer.concat(buf, buf.dataLength)) 442 | } 443 | 444 | // stream.promise().then(() => done, er => emitted error) 445 | promise () { 446 | return new Promise((resolve, reject) => { 447 | this.on(DESTROYED, () => reject(new Error('stream destroyed'))) 448 | this.on('end', () => resolve()) 449 | this.on('error', er => reject(er)) 450 | }) 451 | } 452 | 453 | // for await (let chunk of stream) 454 | [ASYNCITERATOR] () { 455 | const next = () => { 456 | const res = this.read() 457 | if (res !== null) 458 | return Promise.resolve({ done: false, value: res }) 459 | 460 | if (this[EOF]) 461 | return Promise.resolve({ done: true }) 462 | 463 | let resolve = null 464 | let reject = null 465 | const onerr = er => { 466 | this.removeListener('data', ondata) 467 | this.removeListener('end', onend) 468 | reject(er) 469 | } 470 | const ondata = value => { 471 | this.removeListener('error', onerr) 472 | this.removeListener('end', onend) 473 | this.pause() 474 | resolve({ value: value, done: !!this[EOF] }) 475 | } 476 | const onend = () => { 477 | this.removeListener('error', onerr) 478 | this.removeListener('data', ondata) 479 | resolve({ done: true }) 480 | } 481 | const ondestroy = () => onerr(new Error('stream destroyed')) 482 | return new Promise((res, rej) => { 483 | reject = rej 484 | resolve = res 485 | this.once(DESTROYED, ondestroy) 486 | this.once('error', onerr) 487 | this.once('end', onend) 488 | this.once('data', ondata) 489 | }) 490 | } 491 | 492 | return { next } 493 | } 494 | 495 | // for (let chunk of stream) 496 | [ITERATOR] () { 497 | const next = () => { 498 | const value = this.read() 499 | const done = value === null 500 | return { value, done } 501 | } 502 | return { next } 503 | } 504 | 505 | destroy (er) { 506 | if (this[DESTROYED]) { 507 | if (er) 508 | this.emit('error', er) 509 | else 510 | this.emit(DESTROYED) 511 | return this 512 | } 513 | 514 | this[DESTROYED] = true 515 | 516 | // throw away all buffered data, it's never coming out 517 | this.buffer = new Yallist() 518 | this[BUFFERLENGTH] = 0 519 | 520 | if (typeof this.close === 'function' && !this[CLOSED]) 521 | this.close() 522 | 523 | if (er) 524 | this.emit('error', er) 525 | else // if no error to emit, still reject pending promises 526 | this.emit(DESTROYED) 527 | 528 | return this 529 | } 530 | 531 | static isStream (s) { 532 | return !!s && (s instanceof Minipass || s instanceof Stream || 533 | s instanceof EE && ( 534 | typeof s.pipe === 'function' || // readable 535 | (typeof s.write === 'function' && typeof s.end === 'function') // writable 536 | )) 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minipass 2 | 3 | A _very_ minimal implementation of a [PassThrough 4 | stream](https://nodejs.org/api/stream.html#stream_class_stream_passthrough) 5 | 6 | [It's very 7 | fast](https://docs.google.com/spreadsheets/d/1oObKSrVwLX_7Ut4Z6g3fZW-AX1j1-k6w-cDsrkaSbHM/edit#gid=0) 8 | for objects, strings, and buffers. 9 | 10 | Supports pipe()ing (including multi-pipe() and backpressure transmission), 11 | buffering data until either a `data` event handler or `pipe()` is added (so 12 | you don't lose the first chunk), and most other cases where PassThrough is 13 | a good idea. 14 | 15 | There is a `read()` method, but it's much more efficient to consume data 16 | from this stream via `'data'` events or by calling `pipe()` into some other 17 | stream. Calling `read()` requires the buffer to be flattened in some 18 | cases, which requires copying memory. 19 | 20 | There is also no `unpipe()` method. Once you start piping, there is no 21 | stopping it! 22 | 23 | If you set `objectMode: true` in the options, then whatever is written will 24 | be emitted. Otherwise, it'll do a minimal amount of Buffer copying to 25 | ensure proper Streams semantics when `read(n)` is called. 26 | 27 | `objectMode` can also be set by doing `stream.objectMode = true`, or by 28 | writing any non-string/non-buffer data. `objectMode` cannot be set to 29 | false once it is set. 30 | 31 | This is not a `through` or `through2` stream. It doesn't transform the 32 | data, it just passes it right through. If you want to transform the data, 33 | extend the class, and override the `write()` method. Once you're done 34 | transforming the data however you want, call `super.write()` with the 35 | transform output. 36 | 37 | For some examples of streams that extend Minipass in various ways, check 38 | out: 39 | 40 | - [minizlib](http://npm.im/minizlib) 41 | - [fs-minipass](http://npm.im/fs-minipass) 42 | - [tar](http://npm.im/tar) 43 | - [minipass-collect](http://npm.im/minipass-collect) 44 | - [minipass-flush](http://npm.im/minipass-flush) 45 | - [minipass-pipeline](http://npm.im/minipass-pipeline) 46 | - [tap](http://npm.im/tap) 47 | - [tap-parser](http://npm.im/tap) 48 | - [treport](http://npm.im/tap) 49 | - [minipass-fetch](http://npm.im/minipass-fetch) 50 | - [pacote](http://npm.im/pacote) 51 | - [make-fetch-happen](http://npm.im/make-fetch-happen) 52 | - [cacache](http://npm.im/cacache) 53 | - [ssri](http://npm.im/ssri) 54 | - [npm-registry-fetch](http://npm.im/npm-registry-fetch) 55 | - [minipass-json-stream](http://npm.im/minipass-json-stream) 56 | - [minipass-sized](http://npm.im/minipass-sized) 57 | 58 | ## Differences from Node.js Streams 59 | 60 | There are several things that make Minipass streams different from (and in 61 | some ways superior to) Node.js core streams. 62 | 63 | Please read these caveats if you are familiar with noode-core streams and 64 | intend to use Minipass streams in your programs. 65 | 66 | ### Timing 67 | 68 | Minipass streams are designed to support synchronous use-cases. Thus, data 69 | is emitted as soon as it is available, always. It is buffered until read, 70 | but no longer. Another way to look at it is that Minipass streams are 71 | exactly as synchronous as the logic that writes into them. 72 | 73 | This can be surprising if your code relies on `PassThrough.write()` always 74 | providing data on the next tick rather than the current one, or being able 75 | to call `resume()` and not have the entire buffer disappear immediately. 76 | 77 | However, without this synchronicity guarantee, there would be no way for 78 | Minipass to achieve the speeds it does, or support the synchronous use 79 | cases that it does. Simply put, waiting takes time. 80 | 81 | This non-deferring approach makes Minipass streams much easier to reason 82 | about, especially in the context of Promises and other flow-control 83 | mechanisms. 84 | 85 | ### No High/Low Water Marks 86 | 87 | Node.js core streams will optimistically fill up a buffer, returning `true` 88 | on all writes until the limit is hit, even if the data has nowhere to go. 89 | Then, they will not attempt to draw more data in until the buffer size dips 90 | below a minimum value. 91 | 92 | Minipass streams are much simpler. The `write()` method will return `true` 93 | if the data has somewhere to go (which is to say, given the timing 94 | guarantees, that the data is already there by the time `write()` returns). 95 | 96 | If the data has nowhere to go, then `write()` returns false, and the data 97 | sits in a buffer, to be drained out immediately as soon as anyone consumes 98 | it. 99 | 100 | ### Hazards of Buffering (or: Why Minipass Is So Fast) 101 | 102 | Since data written to a Minipass stream is immediately written all the way 103 | through the pipeline, and `write()` always returns true/false based on 104 | whether the data was fully flushed, backpressure is communicated 105 | immediately to the upstream caller. This minimizes buffering. 106 | 107 | Consider this case: 108 | 109 | ```js 110 | const {PassThrough} = require('stream') 111 | const p1 = new PassThrough({ highWaterMark: 1024 }) 112 | const p2 = new PassThrough({ highWaterMark: 1024 }) 113 | const p3 = new PassThrough({ highWaterMark: 1024 }) 114 | const p4 = new PassThrough({ highWaterMark: 1024 }) 115 | 116 | p1.pipe(p2).pipe(p3).pipe(p4) 117 | p4.on('data', () => console.log('made it through')) 118 | 119 | // this returns false and buffers, then writes to p2 on next tick (1) 120 | // p2 returns false and buffers, pausing p1, then writes to p3 on next tick (2) 121 | // p3 returns false and buffers, pausing p2, then writes to p4 on next tick (3) 122 | // p4 returns false and buffers, pausing p3, then emits 'data' and 'drain' 123 | // on next tick (4) 124 | // p3 sees p4's 'drain' event, and calls resume(), emitting 'resume' and 125 | // 'drain' on next tick (5) 126 | // p2 sees p3's 'drain', calls resume(), emits 'resume' and 'drain' on next tick (6) 127 | // p1 sees p2's 'drain', calls resume(), emits 'resume' and 'drain' on next 128 | // tick (7) 129 | 130 | p1.write(Buffer.alloc(2048)) // returns false 131 | ``` 132 | 133 | Along the way, the data was buffered and deferred at each stage, and 134 | multiple event deferrals happened, for an unblocked pipeline where it was 135 | perfectly safe to write all the way through! 136 | 137 | Furthermore, setting a `highWaterMark` of `1024` might lead someone reading 138 | the code to think an advisory maximum of 1KiB is being set for the 139 | pipeline. However, the actual advisory buffering level is the _sum_ of 140 | `highWaterMark` values, since each one has its own bucket. 141 | 142 | Consider the Minipass case: 143 | 144 | ```js 145 | const m1 = new Minipass() 146 | const m2 = new Minipass() 147 | const m3 = new Minipass() 148 | const m4 = new Minipass() 149 | 150 | m1.pipe(m2).pipe(m3).pipe(m4) 151 | m4.on('data', () => console.log('made it through')) 152 | 153 | // m1 is flowing, so it writes the data to m2 immediately 154 | // m2 is flowing, so it writes the data to m3 immediately 155 | // m3 is flowing, so it writes the data to m4 immediately 156 | // m4 is flowing, so it fires the 'data' event immediately, returns true 157 | // m4's write returned true, so m3 is still flowing, returns true 158 | // m3's write returned true, so m2 is still flowing, returns true 159 | // m2's write returned true, so m1 is still flowing, returns true 160 | // No event deferrals or buffering along the way! 161 | 162 | m1.write(Buffer.alloc(2048)) // returns true 163 | ``` 164 | 165 | It is extremely unlikely that you _don't_ want to buffer any data written, 166 | or _ever_ buffer data that can be flushed all the way through. Neither 167 | node-core streams nor Minipass ever fail to buffer written data, but 168 | node-core streams do a lot of unnecessary buffering and pausing. 169 | 170 | As always, the faster implementation is the one that does less stuff and 171 | waits less time to do it. 172 | 173 | ### Immediately emit `end` for empty streams (when not paused) 174 | 175 | If a stream is not paused, and `end()` is called before writing any data 176 | into it, then it will emit `end` immediately. 177 | 178 | If you have logic that occurs on the `end` event which you don't want to 179 | potentially happen immediately (for example, closing file descriptors, 180 | moving on to the next entry in an archive parse stream, etc.) then be sure 181 | to call `stream.pause()` on creation, and then `stream.resume()` once you 182 | are ready to respond to the `end` event. 183 | 184 | ### Emit `end` When Asked 185 | 186 | One hazard of immediately emitting `'end'` is that you may not yet have had 187 | a chance to add a listener. In order to avoid this hazard, Minipass 188 | streams safely re-emit the `'end'` event if a new listener is added after 189 | `'end'` has been emitted. 190 | 191 | Ie, if you do `stream.on('end', someFunction)`, and the stream has already 192 | emitted `end`, then it will call the handler right away. (You can think of 193 | this somewhat like attaching a new `.then(fn)` to a previously-resolved 194 | Promise.) 195 | 196 | To prevent calling handlers multiple times who would not expect multiple 197 | ends to occur, all listeners are removed from the `'end'` event whenever it 198 | is emitted. 199 | 200 | ### Impact of "immediate flow" on Tee-streams 201 | 202 | A "tee stream" is a stream piping to multiple destinations: 203 | 204 | ```js 205 | const tee = new Minipass() 206 | t.pipe(dest1) 207 | t.pipe(dest2) 208 | t.write('foo') // goes to both destinations 209 | ``` 210 | 211 | Since Minipass streams _immediately_ process any pending data through the 212 | pipeline when a new pipe destination is added, this can have surprising 213 | effects, especially when a stream comes in from some other function and may 214 | or may not have data in its buffer. 215 | 216 | ```js 217 | // WARNING! WILL LOSE DATA! 218 | const src = new Minipass() 219 | src.write('foo') 220 | src.pipe(dest1) // 'foo' chunk flows to dest1 immediately, and is gone 221 | src.pipe(dest2) // gets nothing! 222 | ``` 223 | 224 | The solution is to create a dedicated tee-stream junction that pipes to 225 | both locations, and then pipe to _that_ instead. 226 | 227 | ```js 228 | // Safe example: tee to both places 229 | const src = new Minipass() 230 | src.write('foo') 231 | const tee = new Minipass() 232 | tee.pipe(dest1) 233 | tee.pipe(dest2) 234 | src.pipe(tee) // tee gets 'foo', pipes to both locations 235 | ``` 236 | 237 | The same caveat applies to `on('data')` event listeners. The first one 238 | added will _immediately_ receive all of the data, leaving nothing for the 239 | second: 240 | 241 | ```js 242 | // WARNING! WILL LOSE DATA! 243 | const src = new Minipass() 244 | src.write('foo') 245 | src.on('data', handler1) // receives 'foo' right away 246 | src.on('data', handler2) // nothing to see here! 247 | ``` 248 | 249 | Using a dedicated tee-stream can be used in this case as well: 250 | 251 | ```js 252 | // Safe example: tee to both data handlers 253 | const src = new Minipass() 254 | src.write('foo') 255 | const tee = new Minipass() 256 | tee.on('data', handler1) 257 | tee.on('data', handler2) 258 | src.pipe(tee) 259 | ``` 260 | 261 | ## USAGE 262 | 263 | It's a stream! Use it like a stream and it'll most likely do what you 264 | want. 265 | 266 | ```js 267 | const Minipass = require('minipass') 268 | const mp = new Minipass(options) // optional: { encoding, objectMode } 269 | mp.write('foo') 270 | mp.pipe(someOtherStream) 271 | mp.end('bar') 272 | ``` 273 | 274 | ### OPTIONS 275 | 276 | * `encoding` How would you like the data coming _out_ of the stream to be 277 | encoded? Accepts any values that can be passed to `Buffer.toString()`. 278 | * `objectMode` Emit data exactly as it comes in. This will be flipped on 279 | by default if you write() something other than a string or Buffer at any 280 | point. Setting `objectMode: true` will prevent setting any encoding 281 | value. 282 | 283 | ### API 284 | 285 | Implements the user-facing portions of Node.js's `Readable` and `Writable` 286 | streams. 287 | 288 | ### Methods 289 | 290 | * `write(chunk, [encoding], [callback])` - Put data in. (Note that, in the 291 | base Minipass class, the same data will come out.) Returns `false` if 292 | the stream will buffer the next write, or true if it's still in "flowing" 293 | mode. 294 | * `end([chunk, [encoding]], [callback])` - Signal that you have no more 295 | data to write. This will queue an `end` event to be fired when all the 296 | data has been consumed. 297 | * `setEncoding(encoding)` - Set the encoding for data coming of the stream. 298 | This can only be done once. 299 | * `pause()` - No more data for a while, please. This also prevents `end` 300 | from being emitted for empty streams until the stream is resumed. 301 | * `resume()` - Resume the stream. If there's data in the buffer, it is all 302 | discarded. Any buffered events are immediately emitted. 303 | * `pipe(dest)` - Send all output to the stream provided. There is no way 304 | to unpipe. When data is emitted, it is immediately written to any and 305 | all pipe destinations. 306 | * `on(ev, fn)`, `emit(ev, fn)` - Minipass streams are EventEmitters. Some 307 | events are given special treatment, however. (See below under "events".) 308 | * `promise()` - Returns a Promise that resolves when the stream emits 309 | `end`, or rejects if the stream emits `error`. 310 | * `collect()` - Return a Promise that resolves on `end` with an array 311 | containing each chunk of data that was emitted, or rejects if the stream 312 | emits `error`. Note that this consumes the stream data. 313 | * `concat()` - Same as `collect()`, but concatenates the data into a single 314 | Buffer object. Will reject the returned promise if the stream is in 315 | objectMode, or if it goes into objectMode by the end of the data. 316 | * `read(n)` - Consume `n` bytes of data out of the buffer. If `n` is not 317 | provided, then consume all of it. If `n` bytes are not available, then 318 | it returns null. **Note** consuming streams in this way is less 319 | efficient, and can lead to unnecessary Buffer copying. 320 | * `destroy([er])` - Destroy the stream. If an error is provided, then an 321 | `'error'` event is emitted. If the stream has a `close()` method, and 322 | has not emitted a `'close'` event yet, then `stream.close()` will be 323 | called. Any Promises returned by `.promise()`, `.collect()` or 324 | `.concat()` will be rejected. After being destroyed, writing to the 325 | stream will emit an error. No more data will be emitted if the stream is 326 | destroyed, even if it was previously buffered. 327 | 328 | ### Properties 329 | 330 | * `bufferLength` Read-only. Total number of bytes buffered, or in the case 331 | of objectMode, the total number of objects. 332 | * `encoding` The encoding that has been set. (Setting this is equivalent 333 | to calling `setEncoding(enc)` and has the same prohibition against 334 | setting multiple times.) 335 | * `flowing` Read-only. Boolean indicating whether a chunk written to the 336 | stream will be immediately emitted. 337 | * `emittedEnd` Read-only. Boolean indicating whether the end-ish events 338 | (ie, `end`, `prefinish`, `finish`) have been emitted. Note that 339 | listening on any end-ish event will immediateyl re-emit it if it has 340 | already been emitted. 341 | * `writable` Whether the stream is writable. Default `true`. Set to 342 | `false` when `end()` 343 | * `readable` Whether the stream is readable. Default `true`. 344 | * `buffer` A [yallist](http://npm.im/yallist) linked list of chunks written 345 | to the stream that have not yet been emitted. (It's probably a bad idea 346 | to mess with this.) 347 | * `pipes` A [yallist](http://npm.im/yallist) linked list of streams that 348 | this stream is piping into. (It's probably a bad idea to mess with 349 | this.) 350 | * `destroyed` A getter that indicates whether the stream was destroyed. 351 | * `paused` True if the stream has been explicitly paused, otherwise false. 352 | * `objectMode` Indicates whether the stream is in `objectMode`. Once set 353 | to `true`, it cannot be set to `false`. 354 | 355 | ### Events 356 | 357 | * `data` Emitted when there's data to read. Argument is the data to read. 358 | This is never emitted while not flowing. If a listener is attached, that 359 | will resume the stream. 360 | * `end` Emitted when there's no more data to read. This will be emitted 361 | immediately for empty streams when `end()` is called. If a listener is 362 | attached, and `end` was already emitted, then it will be emitted again. 363 | All listeners are removed when `end` is emitted. 364 | * `prefinish` An end-ish event that follows the same logic as `end` and is 365 | emitted in the same conditions where `end` is emitted. Emitted after 366 | `'end'`. 367 | * `finish` An end-ish event that follows the same logic as `end` and is 368 | emitted in the same conditions where `end` is emitted. Emitted after 369 | `'prefinish'`. 370 | * `close` An indication that an underlying resource has been released. 371 | Minipass does not emit this event, but will defer it until after `end` 372 | has been emitted, since it throws off some stream libraries otherwise. 373 | * `drain` Emitted when the internal buffer empties, and it is again 374 | suitable to `write()` into the stream. 375 | * `readable` Emitted when data is buffered and ready to be read by a 376 | consumer. 377 | * `resume` Emitted when stream changes state from buffering to flowing 378 | mode. (Ie, when `resume` is called, `pipe` is called, or a `data` event 379 | listener is added.) 380 | 381 | ### Static Methods 382 | 383 | * `Minipass.isStream(stream)` Returns `true` if the argument is a stream, 384 | and false otherwise. To be considered a stream, the object must be 385 | either an instance of Minipass, or an EventEmitter that has either a 386 | `pipe()` method, or both `write()` and `end()` methods. (Pretty much any 387 | stream in node-land will return `true` for this.) 388 | 389 | ## EXAMPLES 390 | 391 | Here are some examples of things you can do with Minipass streams. 392 | 393 | ### simple "are you done yet" promise 394 | 395 | ```js 396 | mp.promise().then(() => { 397 | // stream is finished 398 | }, er => { 399 | // stream emitted an error 400 | }) 401 | ``` 402 | 403 | ### collecting 404 | 405 | ```js 406 | mp.collect().then(all => { 407 | // all is an array of all the data emitted 408 | // encoding is supported in this case, so 409 | // so the result will be a collection of strings if 410 | // an encoding is specified, or buffers/objects if not. 411 | // 412 | // In an async function, you may do 413 | // const data = await stream.collect() 414 | }) 415 | ``` 416 | 417 | ### collecting into a single blob 418 | 419 | This is a bit slower because it concatenates the data into one chunk for 420 | you, but if you're going to do it yourself anyway, it's convenient this 421 | way: 422 | 423 | ```js 424 | mp.concat().then(onebigchunk => { 425 | // onebigchunk is a string if the stream 426 | // had an encoding set, or a buffer otherwise. 427 | }) 428 | ``` 429 | 430 | ### iteration 431 | 432 | You can iterate over streams synchronously or asynchronously in platforms 433 | that support it. 434 | 435 | Synchronous iteration will end when the currently available data is 436 | consumed, even if the `end` event has not been reached. In string and 437 | buffer mode, the data is concatenated, so unless multiple writes are 438 | occurring in the same tick as the `read()`, sync iteration loops will 439 | generally only have a single iteration. 440 | 441 | To consume chunks in this way exactly as they have been written, with no 442 | flattening, create the stream with the `{ objectMode: true }` option. 443 | 444 | ```js 445 | const mp = new Minipass({ objectMode: true }) 446 | mp.write('a') 447 | mp.write('b') 448 | for (let letter of mp) { 449 | console.log(letter) // a, b 450 | } 451 | mp.write('c') 452 | mp.write('d') 453 | for (let letter of mp) { 454 | console.log(letter) // c, d 455 | } 456 | mp.write('e') 457 | mp.end() 458 | for (let letter of mp) { 459 | console.log(letter) // e 460 | } 461 | for (let letter of mp) { 462 | console.log(letter) // nothing 463 | } 464 | ``` 465 | 466 | Asynchronous iteration will continue until the end event is reached, 467 | consuming all of the data. 468 | 469 | ```js 470 | const mp = new Minipass({ encoding: 'utf8' }) 471 | 472 | // some source of some data 473 | let i = 5 474 | const inter = setInterval(() => { 475 | if (i --> 0) 476 | mp.write(Buffer.from('foo\n', 'utf8')) 477 | else { 478 | mp.end() 479 | clearInterval(inter) 480 | } 481 | }, 100) 482 | 483 | // consume the data with asynchronous iteration 484 | async function consume () { 485 | for await (let chunk of mp) { 486 | console.log(chunk) 487 | } 488 | return 'ok' 489 | } 490 | 491 | consume().then(res => console.log(res)) 492 | // logs `foo\n` 5 times, and then `ok` 493 | ``` 494 | 495 | ### subclass that `console.log()`s everything written into it 496 | 497 | ```js 498 | class Logger extends Minipass { 499 | write (chunk, encoding, callback) { 500 | console.log('WRITE', chunk, encoding) 501 | return super.write(chunk, encoding, callback) 502 | } 503 | end (chunk, encoding, callback) { 504 | console.log('END', chunk, encoding) 505 | return super.end(chunk, encoding, callback) 506 | } 507 | } 508 | 509 | someSource.pipe(new Logger()).pipe(someDest) 510 | ``` 511 | 512 | ### same thing, but using an inline anonymous class 513 | 514 | ```js 515 | // js classes are fun 516 | someSource 517 | .pipe(new (class extends Minipass { 518 | emit (ev, ...data) { 519 | // let's also log events, because debugging some weird thing 520 | console.log('EMIT', ev) 521 | return super.emit(ev, ...data) 522 | } 523 | write (chunk, encoding, callback) { 524 | console.log('WRITE', chunk, encoding) 525 | return super.write(chunk, encoding, callback) 526 | } 527 | end (chunk, encoding, callback) { 528 | console.log('END', chunk, encoding) 529 | return super.end(chunk, encoding, callback) 530 | } 531 | })) 532 | .pipe(someDest) 533 | ``` 534 | 535 | ### subclass that defers 'end' for some reason 536 | 537 | ```js 538 | class SlowEnd extends Minipass { 539 | emit (ev, ...args) { 540 | if (ev === 'end') { 541 | console.log('going to end, hold on a sec') 542 | setTimeout(() => { 543 | console.log('ok, ready to end now') 544 | super.emit('end', ...args) 545 | }, 100) 546 | } else { 547 | return super.emit(ev, ...args) 548 | } 549 | } 550 | } 551 | ``` 552 | 553 | ### transform that creates newline-delimited JSON 554 | 555 | ```js 556 | class NDJSONEncode extends Minipass { 557 | write (obj, cb) { 558 | try { 559 | // JSON.stringify can throw, emit an error on that 560 | return super.write(JSON.stringify(obj) + '\n', 'utf8', cb) 561 | } catch (er) { 562 | this.emit('error', er) 563 | } 564 | } 565 | end (obj, cb) { 566 | if (typeof obj === 'function') { 567 | cb = obj 568 | obj = undefined 569 | } 570 | if (obj !== undefined) { 571 | this.write(obj) 572 | } 573 | return super.end(cb) 574 | } 575 | } 576 | ``` 577 | 578 | ### transform that parses newline-delimited JSON 579 | 580 | ```js 581 | class NDJSONDecode extends Minipass { 582 | constructor (options) { 583 | // always be in object mode, as far as Minipass is concerned 584 | super({ objectMode: true }) 585 | this._jsonBuffer = '' 586 | } 587 | write (chunk, encoding, cb) { 588 | if (typeof chunk === 'string' && 589 | typeof encoding === 'string' && 590 | encoding !== 'utf8') { 591 | chunk = Buffer.from(chunk, encoding).toString() 592 | } else if (Buffer.isBuffer(chunk)) 593 | chunk = chunk.toString() 594 | } 595 | if (typeof encoding === 'function') { 596 | cb = encoding 597 | } 598 | const jsonData = (this._jsonBuffer + chunk).split('\n') 599 | this._jsonBuffer = jsonData.pop() 600 | for (let i = 0; i < jsonData.length; i++) { 601 | let parsed 602 | try { 603 | super.write(parsed) 604 | } catch (er) { 605 | this.emit('error', er) 606 | continue 607 | } 608 | } 609 | if (cb) 610 | cb() 611 | } 612 | } 613 | ``` 614 | --------------------------------------------------------------------------------