├── .gitignore ├── test ├── lib │ ├── without-bb.js │ └── interface-tests.js ├── fun-recursive-fun.js ├── reduce-stream.js ├── async │ ├── is-async.js │ └── fun-async-generator.js ├── for-each-stream.js ├── with.js ├── mini-sync-sink.js ├── line-stream.js ├── json-stream.js ├── ndjson-stream.js ├── index.js ├── fun-duplex.js ├── fun-passthrough.js ├── fun-promised-fun.js ├── fun-array.js ├── fun-generator.js ├── is.js └── construction.js ├── fun-transform.js ├── fun-passthrough.js ├── fun-async-generator.js ├── fun-generator.js ├── mini-sync-sink.js ├── .travis.yml ├── LICENSE ├── for-each-stream.js ├── bench ├── index.js └── map.js ├── fun-array.js ├── line-stream.js ├── is.js ├── package.json ├── mutate-stream.js ├── reduce-stream.js ├── flat-map-stream.js ├── map-stream.js ├── filter-stream.js ├── index.js ├── fun-duplex.js ├── mixin-promise-stream.js ├── stream-promise.js ├── CHANGELOG.md ├── fun-stream.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .nyc_output 4 | *~ 5 | .#* 6 | -------------------------------------------------------------------------------- /test/lib/without-bb.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fun = require('../..') 3 | process.noBB = true 4 | fun.Promise = Promise 5 | /* eslint-disable security/detect-non-literal-require */ 6 | require(process.cwd() + '/' + process.argv[2]) 7 | -------------------------------------------------------------------------------- /test/fun-recursive-fun.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('fun(fun(fun()))', t => { 6 | const readable = fun() 7 | t.is(readable, fun(readable), 'funning a fun stream does nothing') 8 | t.done() 9 | }) 10 | -------------------------------------------------------------------------------- /fun-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Transform = require('stream').Transform 3 | const FunStream = require('./fun-stream.js') 4 | class FunTransform extends Transform { 5 | constructor (opts) { 6 | super({objectMode: true}) 7 | FunTransform.funInit.call(this, opts) 8 | } 9 | } 10 | FunStream.mixin(FunTransform) 11 | 12 | module.exports = FunTransform 13 | -------------------------------------------------------------------------------- /fun-passthrough.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const PassThrough = require('stream').PassThrough 3 | const FunStream = require('./fun-stream.js') 4 | class FunPassThrough extends PassThrough { 5 | constructor (opts) { 6 | super({objectMode: true}) 7 | FunPassThrough.funInit.call(this, opts) 8 | } 9 | } 10 | FunStream.mixin(FunPassThrough) 11 | module.exports = FunPassThrough 12 | -------------------------------------------------------------------------------- /test/reduce-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('sync errors', t => { 6 | return fun([1, 2, 3]).reduce((acc, val) => { 7 | if (val % 2) { 8 | throw new Error('ODD') 9 | } else { 10 | return acc + val 11 | } 12 | }).then(() => t.fail('got error'), () => t.pass('got error')) 13 | }) 14 | -------------------------------------------------------------------------------- /test/async/is-async.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-disable node/no-unsupported-features/es-syntax */ 3 | const test = require('tap').test 4 | const is = require('../../is.js') 5 | 6 | async function * fromAsyncArray (arr) { 7 | for (let ii = 0; ii < arr.length; ++ii) { 8 | yield arr[ii] 9 | } 10 | } 11 | 12 | test('scalar', t => { 13 | t.is(is.scalar(fromAsyncArray()), false, 'scalar') 14 | t.is(is.iterator(fromAsyncArray()), false, 'iterator') 15 | t.is(is.thenable(fromAsyncArray()), false, 'thenable') 16 | t.is(is.plainObject(fromAsyncArray()), false, 'plainObject') 17 | t.is(is.asyncIterator(fromAsyncArray()), true, 'asyncIterator') 18 | t.done() 19 | }) 20 | -------------------------------------------------------------------------------- /test/for-each-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('sync 1', t => { 6 | const values = [1, 2] 7 | return fun([1, 2]).forEach(value => { 8 | t.is(value, values.shift()) 9 | }) 10 | }) 11 | 12 | // second tests conditional loading of mini-sync-sink 13 | test('sync 2', t => { 14 | const values = [1, 2] 15 | return fun([1, 2]).forEach(value => { 16 | t.is(value, values.shift()) 17 | }) 18 | }) 19 | 20 | test('async promise', t => { 21 | const values = [1, 2] 22 | return fun([1, 2]).async().forEach(value => { 23 | t.is(value, values.shift()) 24 | return Promise.resolve() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /fun-async-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Readable = require('stream').Readable 3 | const FunStream = require('./fun-stream.js') 4 | 5 | const ITER = Symbol('iter') 6 | 7 | class FunAsyncGenerator extends Readable { 8 | constructor (iter, opts) { 9 | super({objectMode: true}) 10 | FunAsyncGenerator.funInit.call(this, opts) 11 | this[ITER] = iter 12 | } 13 | _read () { 14 | this[ITER].next().then(current => { 15 | if (current.done) return this.push(null) 16 | if (!this.push(current.value)) return 17 | this._read() 18 | }).catch(err => { 19 | this.emit('error', err) 20 | }) 21 | } 22 | } 23 | FunStream.mixin(FunAsyncGenerator) 24 | module.exports = FunAsyncGenerator 25 | -------------------------------------------------------------------------------- /fun-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Readable = require('stream').Readable 3 | const FunStream = require('./fun-stream.js') 4 | 5 | const ITER = Symbol('iter') 6 | 7 | class FunGenerator extends Readable { 8 | constructor (iter, opts) { 9 | super({objectMode: true}) 10 | FunGenerator.funInit.call(this, opts) 11 | this[ITER] = iter 12 | } 13 | _read () { 14 | while (true) { 15 | try { 16 | let current = this[ITER].next() 17 | if (current.done) return this.push(null) 18 | if (!this.push(current.value)) return 19 | } catch (err) { 20 | this.emit('error', err) 21 | return 22 | } 23 | } 24 | } 25 | } 26 | FunStream.mixin(FunGenerator) 27 | module.exports = FunGenerator 28 | -------------------------------------------------------------------------------- /mini-sync-sink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const EventEmitter = require('events').EventEmitter 3 | const mixinPromiseStream = require('./mixin-promise-stream.js') 4 | 5 | class MiniSyncSink extends EventEmitter { 6 | constructor (opts) { 7 | super(opts) 8 | mixinPromiseStream(this, opts) 9 | if (opts.write) this._write = opts.write 10 | } 11 | write (data, encoding, next) { 12 | try { 13 | this._write(data, encoding) 14 | /* istanbul ignore next */ 15 | if (next) next() 16 | return true 17 | } catch (err) { 18 | this.emit('error', err) 19 | return false 20 | } 21 | } 22 | end () { 23 | this.emit('prefinish') 24 | this.emit('finish') 25 | } 26 | } 27 | 28 | module.exports = MiniSyncSink 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | matrix: 4 | include: 5 | - node_js: "11" 6 | install: 7 | - "mv node_modules/.cache . || true" 8 | - "npm ci" 9 | - "mv .cache node_modules/ || true" 10 | script: 11 | - "npm t" 12 | - node_js: "10" 13 | script: 14 | - "npm run test-no-coverage" 15 | - node_js: "8" 16 | script: 17 | - "npm run test-old-node" 18 | - node_js: "6" 19 | script: 20 | - "npm run test-old-node" 21 | - node_js: "4" 22 | install: 23 | - "mv package-lock.json npm-shrinkwrap.json" 24 | - "npm i" 25 | script: 26 | - "npm run test-old-node" 27 | cache: 28 | directories: 29 | - node_modules/.cache 30 | - ~/.npm 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Rebecca Turner 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /test/with.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('../index.js') 4 | 5 | test('with', t => { 6 | return fun.with(st => { 7 | let count = 0 8 | return new Promise(function each (resolve) { 9 | if (++count === 5) return resolve() 10 | st.write(count) 11 | setTimeout(each, 100, resolve) 12 | }) 13 | }).list().then(result => { 14 | t.isDeeply(result, [ 1, 2, 3, 4 ]) 15 | }) 16 | }) 17 | 18 | test('with-bad', t => { 19 | t.throws(() => fun.with((s) => 'nah')) 20 | t.done() 21 | }) 22 | 23 | test('with-err', t => { 24 | t.plan(1) 25 | return fun.with(st => { 26 | let count = 0 27 | return new Promise(function each (resolve, reject) { 28 | if (++count === 5) return reject(new Error()) 29 | st.write(count) 30 | setTimeout(each, 100, resolve, reject) 31 | }) 32 | }).list().then(result => { 33 | t.fail() 34 | }, () => { 35 | t.pass() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /for-each-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Writable = require('stream').Writable 3 | const mixinPromiseStream = require('./mixin-promise-stream.js') 4 | let MiniSyncSink 5 | let FunStream 6 | 7 | module.exports = ForEachStream 8 | 9 | function ForEachStream (consumeWith, opts) { 10 | if (!FunStream) FunStream = require('./fun-stream.js') 11 | if (FunStream.isAsync(consumeWith, 1, opts)) { 12 | return new ForEachStreamAsync(Object.assign({consumeWith: consumeWith}, opts)) 13 | } else { 14 | if (!MiniSyncSink) MiniSyncSink = require('./mini-sync-sink.js') 15 | return new MiniSyncSink(Object.assign({write: consumeWith}, opts)) 16 | } 17 | } 18 | 19 | class ForEachStreamAsync extends Writable { 20 | constructor (opts) { 21 | super({objectMode: true}) 22 | mixinPromiseStream(this, opts) 23 | this.consumeWith = opts.consumeWith 24 | } 25 | _write (data, encoding, next) { 26 | const result = this.consumeWith(data, next) 27 | if (result && result.then) return result.then(keep => next(null, keep), next) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Benchmark = require('benchmark') 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const suite = new Benchmark.Suite({ 8 | onCycle (event) { 9 | const bench = event.target 10 | console.log(` ${bench.name}`) 11 | console.log('------------------------------------------------') 12 | if (bench.error) { 13 | console.log('Error:', bench.error.message || bench.error) 14 | } else { 15 | console.log(` ${ 16 | bench.hz.toFixed(bench.hz < 100 ? 2 : 0) 17 | } ops/s @ ~${ 18 | (bench.stats.mean * 1000).toFixed(3) 19 | }ms/op`) 20 | console.log(` Sampled ${ 21 | bench.stats.sample.length 22 | } in ${ 23 | bench.times.elapsed.toFixed(2)}s.`) 24 | } 25 | console.log('================================================') 26 | } 27 | }) 28 | 29 | fs.readdir(__dirname, (err, files) => { 30 | if (err) { throw err } 31 | files.forEach(f => { 32 | if (f[0] !== '.' && path.extname(f) === '.js' && f !== 'index.js') { 33 | require('./' + f)(suite) 34 | } 35 | }) 36 | suite.run({async: true}) 37 | }) 38 | -------------------------------------------------------------------------------- /test/mini-sync-sink.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const PassThrough = require('stream').PassThrough 4 | const MiniSyncSink = require('../mini-sync-sink.js') 5 | 6 | test('basic', t => { 7 | let expected = ['abc', 'def', 'ghi'] 8 | const font = new PassThrough({objectMode: true}) 9 | const sink = new MiniSyncSink({ 10 | Promise, 11 | write (data) { 12 | t.is(data, expected.shift(), 'got expected data') 13 | } 14 | }) 15 | sink.on('finish', t.end) 16 | font.pipe(sink) 17 | font.write('abc') 18 | font.write('def') 19 | font.end('ghi') 20 | }) 21 | 22 | test('error', t => { 23 | let expected = ['abc', 'def'] 24 | const font = new PassThrough({objectMode: true}) 25 | const sink = new MiniSyncSink({ 26 | Promise, 27 | write (data, cb) { 28 | t.is(data, expected.shift(), 'got expected data') 29 | if (data === 'def') { 30 | throw new Error('boom') 31 | } 32 | } 33 | }) 34 | sink.on('error', () => { t.pass('got error'); t.end() }) 35 | sink.on('finish', () => t.fail('finished')) 36 | font.pipe(sink) 37 | font.write('abc') 38 | font.write('def') 39 | }) 40 | -------------------------------------------------------------------------------- /test/line-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('line-stream', t => { 6 | const st = fun() 7 | st.write('abc') 8 | st.write('def\n') 9 | st.write('ghi\njkl') 10 | st.write('mno') 11 | st.write('\npqr') 12 | st.write('\n\n') 13 | st.end() 14 | return st.lines().collect().then(values => { 15 | t.isDeeply(values, [ 'abcdef', 'ghi', 'jklmno', 'pqr', '' ]) 16 | }) 17 | }) 18 | 19 | test('no-new-lines', t => { 20 | const st = fun() 21 | st.write('abc') 22 | st.write('def') 23 | st.write('ghi') 24 | st.end() 25 | return st.lines().collect().then(values => { 26 | t.isDeeply(values, [ 'abcdefghi' ]) 27 | }) 28 | }) 29 | 30 | test('CRLF line-stream', t => { 31 | const st = fun() 32 | st.write('abc') 33 | st.write('def\r\n') 34 | st.write('ghi\r\njkl') 35 | st.write('mno') 36 | st.write('\r\npqr') 37 | st.write('\r\n') 38 | st.write('test\r') 39 | st.write('\n') 40 | st.write('\r\n\r\n') 41 | st.end() 42 | return st.lines().collect().then(values => { 43 | t.isDeeply(values, [ 'abcdef', 'ghi', 'jklmno', 'pqr', 'test', '', '' ]) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/json-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('json-stream', t => { 6 | const st = fun() 7 | st.write('[{"abc"') 8 | st.write(':true},\n') 9 | st.write('{"ghi":true},{"jkl') 10 | st.write('":true') 11 | st.write('},{"pqr":true}') 12 | st.write('\n]\n') 13 | st.end() 14 | return st.json().then(values => { 15 | t.isDeeply(values, [ {abc: true}, {ghi: true}, {jkl: true}, {pqr: true} ]) 16 | }) 17 | }) 18 | 19 | test('from-json-stream', t => { 20 | const st = fun() 21 | st.write('[{"abc"') 22 | st.write(':true},\n') 23 | st.write('{"ghi":true},{"jkl') 24 | st.write('":true') 25 | st.write('},{"pqr":true}') 26 | st.write('\n]\n') 27 | st.end() 28 | // st.ndjson() is an alias for this, but coverage 29 | return st.fromJson().then(values => { 30 | t.isDeeply(values, [ {abc: true}, {ghi: true}, {jkl: true}, {pqr: true} ]) 31 | }) 32 | }) 33 | 34 | test('to-json-stream', t => { 35 | const st = fun() 36 | st.write({abc: true}) 37 | st.write({ghi: true}) 38 | st.end() 39 | return st.toJson().concat().then(result => { 40 | t.isDeeply(result, '[{"abc":true},{"ghi":true}]') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/ndjson-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | 5 | test('ndjson-stream', t => { 6 | const st = fun() 7 | st.write('{"abc"') 8 | st.write(':true}\n') 9 | st.write('{"ghi":true}\n{"jkl') 10 | st.write('":true') 11 | st.write('}\n{"pqr":true}') 12 | st.write('\n\n') 13 | st.end() 14 | return st.ndjson().collect().then(values => { 15 | t.isDeeply(values, [ {abc: true}, {ghi: true}, {jkl: true}, {pqr: true} ]) 16 | }) 17 | }) 18 | 19 | test('from-ndjson-stream', t => { 20 | const st = fun() 21 | st.write('{"abc"') 22 | st.write(':true}\n') 23 | st.write('{"ghi":true}\n{"jkl') 24 | st.write('":true') 25 | st.write('}\n{"pqr":true}') 26 | st.write('\n\n') 27 | st.end() 28 | // st.ndjson() is an alias for this, but coverage 29 | return st.fromNdjson().collect().then(values => { 30 | t.isDeeply(values, [ {abc: true}, {ghi: true}, {jkl: true}, {pqr: true} ]) 31 | }) 32 | }) 33 | 34 | test('to-ndjson-stream', t => { 35 | const st = fun() 36 | st.write({abc: true}) 37 | st.write({ghi: true}) 38 | st.end() 39 | return st.toNdjson().concat().then(result => { 40 | t.isDeeply(result, '{"abc":true}\n{"ghi":true}\n') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /fun-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let fun 3 | const Readable = require('stream').Readable 4 | const FunStream = require('./fun-stream.js') 5 | const OPTS = require('./fun-stream.js').OPTS 6 | const DATA = Symbol('data') 7 | const INDEX = Symbol('index') 8 | 9 | class FunArray extends Readable { 10 | constructor (data, opts) { 11 | super({objectMode: true}) 12 | FunArray.funInit.call(this, opts) 13 | this[DATA] = data 14 | this[INDEX] = 0 15 | } 16 | _read () { 17 | while (this[INDEX] < this[DATA].length) { 18 | if (!this.push(this[DATA][this[INDEX]++])) { 19 | return 20 | } 21 | } 22 | this.push(null) 23 | } 24 | forEach (forEachWith, forEachOpts) { 25 | const opts = Object.assign({}, this[OPTS], forEachOpts || {}) 26 | if (FunStream.isAsync(forEachWith, 1, opts)) { 27 | return FunStream.prototype.forEach.call(this, forEachWith, forEachOpts) 28 | } else { 29 | if (!fun) fun = require('./index.js') 30 | return fun(new opts.Promise(resolve => { 31 | process.nextTick(() => { 32 | this[DATA].forEach(v => forEachWith(v)) 33 | resolve() 34 | }) 35 | })) 36 | } 37 | } 38 | } 39 | FunStream.mixin(FunArray) 40 | module.exports = FunArray 41 | -------------------------------------------------------------------------------- /line-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const FunTransform = require('./fun-transform.js') 3 | 4 | class LineStream extends FunTransform { 5 | constructor (filterWith, opts) { 6 | super(opts) 7 | // as an array, not a string, to let data chunks be Strings or Buffers 8 | this.buffer = [] 9 | } 10 | _transform (data, encoding, next) { 11 | let lastIndex = 0 12 | let newlineLoc = data.indexOf('\n') 13 | if (newlineLoc === -1) { 14 | this.buffer.push(data) 15 | return next() 16 | } 17 | while (newlineLoc !== -1) { 18 | let chunk 19 | if (this.buffer.length > 0) { 20 | chunk = this.buffer.join('') + data.slice(lastIndex, newlineLoc) 21 | this.buffer = [] 22 | } else { 23 | chunk = data.slice(lastIndex, newlineLoc) 24 | } 25 | if (chunk[chunk.length - 1] === '\r') chunk = chunk.slice(0, -1) 26 | this.push(chunk) 27 | lastIndex = newlineLoc + 1 28 | newlineLoc = data.indexOf('\n', lastIndex) 29 | } 30 | if (lastIndex < data.length) this.buffer = [data.slice(lastIndex)] 31 | next() 32 | } 33 | _flush (next) { 34 | if (this.buffer.length > 0) this.push(this.buffer.join('')) 35 | this.buffer = null 36 | next() 37 | } 38 | } 39 | 40 | module.exports = LineStream 41 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('../index.js') 4 | const requireInject = require('require-inject') 5 | const FunPassThrough = require('../fun-passthrough.js') 6 | const stream = require('stream') 7 | const is = require('../is.js') 8 | 9 | function freshfun () { 10 | const fun = requireInject('../index.js') 11 | if (process.noBB) fun.Promise = Promise 12 | return fun 13 | } 14 | 15 | if (process.noBB) { 16 | // this just ensures that test/lib/without-bb.js is doing its job 17 | test('no-blue', t => { 18 | t.is(fun.Promise, Promise, 'used regular promise') 19 | t.done() 20 | }) 21 | } 22 | 23 | test('fun.FunStream', t => { 24 | const fun = freshfun() 25 | t.ok(fun() instanceof FunPassThrough, 'blank fun is fun') 26 | t.is(fun.FunStream, FunPassThrough, 'FunStream base class is right') 27 | t.done() 28 | }) 29 | 30 | test('fun(Readable)', t => { 31 | const fun = freshfun() 32 | t.ok(is.Readable(fun(new stream.Readable()))) 33 | // and again, for coverage 34 | t.ok(is.Readable(fun(new stream.Readable()))) 35 | t.done() 36 | }) 37 | 38 | test('fun(Writable)', t => { 39 | const fun = freshfun() 40 | t.ok(is.Writable(fun(new stream.Writable()))) 41 | // and again, for coverage 42 | t.ok(is.Writable(fun(new stream.Writable()))) 43 | t.done() 44 | }) 45 | 46 | test('fun({})', t => { 47 | const fun = freshfun() 48 | t.ok(fun({}) instanceof FunPassThrough) 49 | t.done() 50 | }) 51 | -------------------------------------------------------------------------------- /is.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const isaStream = require('isa-stream') 3 | 4 | exports.scalar = isScalar 5 | exports.iterator = isIterator 6 | exports.asyncIterator = isAsyncIterator 7 | exports.thenable = isThenable 8 | exports.plainObject = isPlainObject 9 | exports.Readable = isaStream.Readable 10 | exports.Writable = isaStream.Writable 11 | exports.Duplex = isaStream.Duplex 12 | 13 | function isScalar (value) { 14 | if (value == null) return true 15 | if (Buffer.isBuffer(value)) return true 16 | switch (typeof value) { 17 | case 'string': 18 | case 'number': 19 | case 'bigint': 20 | case 'boolean': 21 | case 'symbol': 22 | return true 23 | default: 24 | return false 25 | } 26 | } 27 | 28 | function isIterator (value) { 29 | try { 30 | return Symbol.iterator in value && 'next' in value 31 | } catch (_) { 32 | return false 33 | } 34 | } 35 | 36 | function isAsyncIterator (value) { 37 | try { 38 | return Symbol.asyncIterator in value && 'next' in value 39 | } catch (_) { 40 | return false 41 | } 42 | } 43 | 44 | function isThenable (value) { 45 | try { 46 | return 'then' in value 47 | } catch (_) { 48 | return false 49 | } 50 | } 51 | 52 | function isPlainObject (value) { 53 | if (value == null) return false 54 | if (typeof value !== 'object') return false 55 | if (Array.isArray(value)) return false 56 | if (Buffer.isBuffer(value)) return false 57 | if (isIterator(value)) return false 58 | if (isAsyncIterator(value)) return false 59 | if (isaStream.Readable(value)) return false 60 | if (isaStream.Writable(value)) return false 61 | if (isThenable(value)) return false 62 | return true 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "funstream", 3 | "version": "4.2.0", 4 | "description": "Funstream gives you iteratorish methods on your streams.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "iarna-standard *.js test/*.js test/*/*.js && tap -J --branches=67 --lines=75 --functions=66 --statements=72 test/*.js test/async/*.js && tap -J --node-arg=test/lib/without-bb.js test/*.js test/async/*.js", 8 | "test-no-coverage": "tap -J test/*.js test/async/*.js", 9 | "test-old-node": "tap -J test/*.js" 10 | }, 11 | "keywords": [], 12 | "author": "Rebecca Turner (http://re-becca.org/)", 13 | "license": "ISC", 14 | "dependencies": { 15 | "isa-stream": "^1.1.3" 16 | }, 17 | "devDependencies": { 18 | "@iarna/standard": "^2.0.1", 19 | "benchmark": "^2.1.4", 20 | "bluebird": "^3.5.4", 21 | "qw": "^1.0.1", 22 | "require-inject": "^1.4.4", 23 | "tap": "^11.0.0", 24 | "through2": "^2.0.3" 25 | }, 26 | "files": [ 27 | "filter-stream.js", 28 | "flat-map-stream.js", 29 | "for-each-stream.js", 30 | "fun-array.js", 31 | "fun-async-generator.js", 32 | "fun-duplex.js", 33 | "fun-generator.js", 34 | "fun-passthrough.js", 35 | "fun-stream.js", 36 | "fun-transform.js", 37 | "index.js", 38 | "is.js", 39 | "map-stream.js", 40 | "mini-sync-sink.js", 41 | "mixin-promise-stream.js", 42 | "mutate-stream.js", 43 | "reduce-stream.js", 44 | "stream-promise.js", 45 | "line-stream.js" 46 | ], 47 | "directories": { 48 | "test": "test" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/iarna/funstream.git" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/iarna/funstream/issues" 56 | }, 57 | "homepage": "https://npmjs.com/package/funstream" 58 | } 59 | -------------------------------------------------------------------------------- /mutate-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let FunStream 3 | const MapStream = require('./map-stream.js') 4 | 5 | module.exports = MutateStream 6 | 7 | function MutateStream (mapWith, opts) { 8 | if (!FunStream) FunStream = require('./fun-stream.js') 9 | if (FunStream.isAsync(mapWith, 1, opts)) { 10 | return new MutateStreamAsync(mapWith, opts) 11 | } else { 12 | return new MutateStreamSync(mapWith, opts) 13 | } 14 | } 15 | const MAPS = MapStream.MAPS 16 | 17 | class MutateStreamAsync extends MapStream.Async { 18 | _runMaps (data, nextMutate, next) { 19 | try { 20 | if (nextMutate >= this[MAPS].length) { 21 | this.push(data) 22 | return next() 23 | } 24 | const handleResult = err => { 25 | if (err) { 26 | return next(err) 27 | } else { 28 | this._runMaps(data, nextMutate + 1, next) 29 | } 30 | } 31 | const result = this[MAPS][nextMutate](data, handleResult) 32 | if (result && result.then) return result.then(keep => handleResult(null, keep), handleResult) 33 | } catch (err) { 34 | next(err) 35 | } 36 | } 37 | mutate (mutateWith, opts) { 38 | if (!FunStream) FunStream = require('./fun-stream.js') 39 | if (FunStream.isAsync(mutateWith, 1, opts)) { 40 | this[MAPS].push(mutateWith) 41 | return this 42 | } else { 43 | return super.mutate(mutateWith, opts) 44 | } 45 | } 46 | } 47 | 48 | class MutateStreamSync extends MapStream.Sync { 49 | _transform (data, encoding, next) { 50 | try { 51 | this[MAPS].forEach(fn => fn(data)) 52 | this.push(data) 53 | next() 54 | } catch (err) { 55 | next(err) 56 | } 57 | } 58 | mutate (mutateWith, opts) { 59 | if (!FunStream) FunStream = require('./fun-stream.js') 60 | if (FunStream.isAsync(mutateWith, 1, opts)) { 61 | return super.mutate(mutateWith, opts) 62 | } else { 63 | this[MAPS].push(mutateWith) 64 | return this 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /reduce-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Writable = require('stream').Writable 3 | const MiniSyncSink = require('./mini-sync-sink') 4 | const mixinPromiseStream = require('./mixin-promise-stream.js') 5 | let FunStream 6 | 7 | module.exports = ReduceStream 8 | 9 | function ReduceStream (reduceWith, initial, opts) { 10 | if (!FunStream) FunStream = require('./fun-stream.js') 11 | if (FunStream.isAsync(reduceWith, 2, opts)) { 12 | return new ReduceStreamAsync(reduceWith, initial, opts) 13 | } else { 14 | return new ReduceStreamSync(reduceWith, initial, opts) 15 | } 16 | } 17 | 18 | class ReduceStreamAsync extends Writable { 19 | constructor (reduceWith, initial, opts) { 20 | super({objectMode: true}) 21 | this[FunStream.OPTS] = opts 22 | mixinPromiseStream(this, opts) 23 | this.reduceWith = reduceWith 24 | this.acc = initial 25 | this.once('prefinish', () => this.emit('result', this.acc)) 26 | } 27 | _write (data, encoding, next) { 28 | if (this.acc == null) { 29 | this.acc = data 30 | next() 31 | } else { 32 | const handleResult = (err, value) => { 33 | this.acc = value 34 | next(err) 35 | } 36 | const result = this.reduceWith(this.acc, data, handleResult) 37 | if (result && result.then) return result.then(keep => handleResult(null, keep), handleResult) 38 | } 39 | } 40 | } 41 | 42 | class ReduceStreamSync extends MiniSyncSink { 43 | constructor (reduceWith, initial, opts) { 44 | super(opts) 45 | this[FunStream.OPTS] = opts 46 | this.reduceWith = reduceWith 47 | this.acc = initial 48 | } 49 | write (data, encoding, next) { 50 | if (this.acc == null) { 51 | this.acc = data 52 | } else { 53 | try { 54 | this.acc = this.reduceWith(this.acc, data) 55 | } catch (err) { 56 | this.emit('error', err) 57 | return false 58 | } 59 | } 60 | /* istanbul ignore next */ 61 | if (next) next() 62 | return true 63 | } 64 | end () { 65 | this.emit('result', this.acc) 66 | super.end() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /flat-map-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const FunTransform = require('./fun-transform.js') 3 | let FunStream 4 | 5 | module.exports = FlatMapStream 6 | 7 | const MAP = Symbol('map') 8 | 9 | function FlatMapStream (mapWith, opts) { 10 | if (!FunStream) FunStream = require('./fun-stream.js') 11 | if (FunStream.isAsync(mapWith, 1, opts)) { 12 | return new FlatMapStreamAsync(mapWith, opts) 13 | } else { 14 | return new FlatMapStreamSync(mapWith, opts) 15 | } 16 | } 17 | 18 | class FlatMapStreamAsync extends FunTransform { 19 | constructor (mapWith, opts) { 20 | super(opts) 21 | this[MAP] = mapWith 22 | } 23 | _transform (data, encoding, next) { 24 | const handleResult = (err, results) => { 25 | if (err) return next(err) 26 | if (Array.isArray(results)) { 27 | results.forEach(v => this.push(v)) 28 | } else if (results && typeof results === 'object' && Symbol.iterator in results) { 29 | const ii = results[Symbol.iterator]() 30 | while (true) { 31 | const rr = ii.next() 32 | if (rr.done) break 33 | this.push(rr.value) 34 | } 35 | } else { 36 | this.push(results) 37 | } 38 | next() 39 | } 40 | try { 41 | const result = this[MAP](data, handleResult) 42 | if (result && result.then) return result.then(keep => handleResult(null, keep), handleResult) 43 | } catch (err) { 44 | return handleResult(err) 45 | } 46 | } 47 | } 48 | 49 | class FlatMapStreamSync extends FunTransform { 50 | constructor (mapWith, opts) { 51 | super(opts) 52 | this[MAP] = mapWith 53 | } 54 | _transform (data, encoding, next) { 55 | try { 56 | const results = this[MAP](data) 57 | if (Array.isArray(results)) { 58 | results.forEach(v => this.push(v)) 59 | } else if (results && typeof results === 'object' && Symbol.iterator in results) { 60 | const ii = results[Symbol.iterator]() 61 | while (true) { 62 | const rr = ii.next() 63 | if (rr.done) break 64 | this.push(rr.value) 65 | } 66 | } else { 67 | this.push(results) 68 | } 69 | next() 70 | } catch (err) { 71 | next(err) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /map-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let FunStream 3 | const FunTransform = require('./fun-transform.js') 4 | 5 | module.exports = MapStream 6 | const MAPS = Symbol('maps') 7 | 8 | function MapStream (mapWith, opts) { 9 | if (!FunStream) FunStream = require('./fun-stream.js') 10 | if (FunStream.isAsync(mapWith, 1, opts)) { 11 | return new MapStreamAsync(mapWith, opts) 12 | } else { 13 | return new MapStreamSync(mapWith, opts) 14 | } 15 | } 16 | MapStream.MAPS = MAPS 17 | 18 | class MapStreamAsync extends FunTransform { 19 | constructor (mapWith, opts) { 20 | super(opts) 21 | this[MAPS] = [mapWith] 22 | } 23 | _transform (data, encoding, next) { 24 | this._runMaps(data, 0, next) 25 | } 26 | _runMaps (data, nextMap, next) { 27 | try { 28 | if (nextMap >= this[MAPS].length) { 29 | this.push(data) 30 | return next() 31 | } 32 | const handleResult = (err, value) => { 33 | if (err) { 34 | return next(err) 35 | } else { 36 | this._runMaps(value, nextMap + 1, next) 37 | } 38 | } 39 | const result = this[MAPS][nextMap](data, handleResult) 40 | if (result && result.then) return result.then(keep => handleResult(null, keep), handleResult) 41 | } catch (err) { 42 | next(err) 43 | } 44 | } 45 | map (mapWith, opts) { 46 | if (!FunStream) FunStream = require('./fun-stream.js') 47 | if (FunStream.isAsync(mapWith, 1, opts)) { 48 | this[MAPS].push(mapWith) 49 | return this 50 | } else { 51 | return super.map(mapWith, opts) 52 | } 53 | } 54 | } 55 | MapStream.Async = MapStreamAsync 56 | 57 | class MapStreamSync extends FunTransform { 58 | constructor (mapWith, opts) { 59 | super(opts) 60 | this[MAPS] = [mapWith] 61 | } 62 | _transform (data, encoding, next) { 63 | try { 64 | this.push(this[MAPS].reduce((data, fn) => fn(data), data)) 65 | next() 66 | } catch (err) { 67 | next(err) 68 | } 69 | } 70 | map (mapWith, opts) { 71 | if (!FunStream) FunStream = require('./fun-stream.js') 72 | if (FunStream.isAsync(mapWith, 1, opts)) { 73 | return super.map(mapWith, opts) 74 | } else { 75 | this[MAPS].push(mapWith) 76 | return this 77 | } 78 | } 79 | } 80 | MapStream.Sync = MapStreamSync 81 | -------------------------------------------------------------------------------- /filter-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const FunTransform = require('./fun-transform.js') 3 | let FunStream 4 | 5 | module.exports = FilterStream 6 | 7 | function FilterStream (filterWith, opts) { 8 | if (!FunStream) FunStream = require('./fun-stream.js') 9 | if (FunStream.isAsync(filterWith, 1, opts)) { 10 | return new FilterStreamAsync(filterWith, opts) 11 | } else { 12 | return new FilterStreamSync(filterWith, opts) 13 | } 14 | } 15 | 16 | class FilterStreamAsync extends FunTransform { 17 | constructor (filterWith, opts) { 18 | super(opts) 19 | this.filters = [filterWith] 20 | } 21 | _transform (data, encoding, next) { 22 | this._runFilters(data, true, 0, next) 23 | } 24 | _runFilters (data, keep, nextFilter, next) { 25 | if (!keep) return next() 26 | if (nextFilter >= this.filters.length) { 27 | this.push(data) 28 | return next() 29 | } 30 | try { 31 | const handleResult = (err, keep) => { 32 | if (err) { 33 | return next(err) 34 | } else { 35 | this._runFilters(data, keep, nextFilter + 1, next) 36 | } 37 | } 38 | 39 | const result = this.filters[nextFilter](data, handleResult) 40 | if (result && result.then) return result.then(keep => handleResult(null, keep), handleResult) 41 | } catch (err) { 42 | return next(err) 43 | } 44 | } 45 | filter (filterWith, opts) { 46 | if (!FunStream) FunStream = require('./fun-stream.js') 47 | if (FunStream.isAsync(filterWith, 1, opts)) { 48 | this.filters.push(filterWith) 49 | return this 50 | } else { 51 | return super.filter(filterWith, opts) 52 | } 53 | } 54 | } 55 | 56 | class FilterStreamSync extends FunTransform { 57 | constructor (filterWith, opts) { 58 | super(opts) 59 | this.filters = [filterWith] 60 | } 61 | _transform (data, encoding, next) { 62 | try { 63 | if (this.filters.every(fn => fn(data))) { 64 | this.push(data, encoding) 65 | } 66 | next() 67 | } catch (err) { 68 | next(err) 69 | } 70 | } 71 | filter (filterWith, opts) { 72 | if (!FunStream) FunStream = require('./fun-stream.js') 73 | if (FunStream.isAsync(filterWith, 1, opts)) { 74 | return super.filter(filterWith, opts) 75 | } else { 76 | this.filters.push(filterWith) 77 | return this 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/fun-duplex.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const isaReadable = require('isa-stream').Readable 6 | const isaWritable = require('isa-stream').Writable 7 | const streamTests = require('./lib/interface-tests.js').streamTests 8 | const promiseTests = require('./lib/interface-tests.js').promiseTests 9 | 10 | test('identity', t => { 11 | const arr = fun([[11], [12], [13]]).pipe(fun(stream => stream.flat().map(v => v - 10))) 12 | t.is(Boolean(isaReadable(arr)), true, 'fun-array: is readable') 13 | t.is(Boolean(isaWritable(arr)), true, 'fun-array: is writable') 14 | t.is(Boolean(FunStream.isFun(arr)), true, 'fun-array: isFun') 15 | t.done() 16 | }) 17 | 18 | streamTests(test, () => fun([[11], [12], [13]]).pipe(fun(stream => stream.flat().map(v => v - 10))), { 19 | pipe: {expected: [1, 2, 3]}, 20 | head: {expected: [1, 2]}, 21 | forEach: {expected: [1, 2, 3]}, 22 | filter: {with: [v => v > 1], expected: [2, 3]}, 23 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 24 | flat: {create: () => fun([[1, 2], [3, 4]]), expected: [1, 2, 3, 4]}, 25 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 26 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 27 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 28 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 29 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 30 | list: {expected: [1, 2, 3]}, 31 | grab: {create: () => fun([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 32 | sort: {create: () => fun([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 33 | concat: {expected: ['123'], asyncSkip: true} 34 | }) 35 | 36 | promiseTests(test, () => fun([[11], [12], [13]]).pipe(fun(stream => stream.flat().map(v => v - 10))), { 37 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 38 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 39 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 40 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 41 | list: {expected: [1, 2, 3]}, 42 | grab: {create: () => fun([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 43 | sort: {create: () => fun([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 44 | concat: {expected: '123', asyncSkip: true} 45 | }) 46 | -------------------------------------------------------------------------------- /test/fun-passthrough.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const isaReadable = require('isa-stream').Readable 6 | const isaWritable = require('isa-stream').Writable 7 | const streamTests = require('./lib/interface-tests.js').streamTests 8 | const promiseTests = require('./lib/interface-tests.js').promiseTests 9 | const Bluebird = require('bluebird') 10 | 11 | function fromArray (arr) { 12 | const stream = fun() 13 | Bluebird.each(arr, v => { 14 | stream.write(v) 15 | }).then(() => { 16 | stream.end() 17 | }) 18 | return stream 19 | } 20 | 21 | test('identity', t => { 22 | const gen = fromArray([1, 2, 3]) 23 | t.is(Boolean(isaReadable(gen)), true, 'fun-passthrough: is readable') 24 | t.is(Boolean(isaWritable(gen)), true, 'fun-passthrough: is writable') 25 | t.is(Boolean(FunStream.isFun(gen)), true, 'fun-passthrough: isFun') 26 | t.done() 27 | }) 28 | 29 | streamTests(test, () => fromArray([1, 2, 3]), { 30 | pipe: {expected: [1, 2, 3]}, 31 | head: {expected: [1, 2]}, 32 | forEach: {expected: [1, 2, 3]}, 33 | filter: {with: [v => v > 1], expected: [2, 3]}, 34 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 35 | flat: {create: () => fromArray([[1, 2], [3, 4]]), expected: [1, 2, 3, 4]}, 36 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 37 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 38 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 39 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 40 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 41 | list: {expected: [1, 2, 3]}, 42 | grab: {create: () => fromArray([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 43 | sort: {create: () => fromArray([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 44 | concat: {expected: ['123'], asyncSkip: true} 45 | }) 46 | 47 | promiseTests(test, () => fromArray([1, 2, 3]), { 48 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 49 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 50 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 51 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 52 | list: {expected: [1, 2, 3]}, 53 | grab: {create: () => fromArray([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 54 | sort: {create: () => fromArray([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 55 | concat: {expected: '123', asyncSkip: true} 56 | }) 57 | -------------------------------------------------------------------------------- /test/fun-promised-fun.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const isaReadable = require('isa-stream').Readable 6 | const isaWritable = require('isa-stream').Writable 7 | const streamTests = require('./lib/interface-tests.js').streamTests 8 | const promiseTests = require('./lib/interface-tests.js').promiseTests 9 | const Bluebird = require('bluebird') 10 | 11 | function fromArray (arr) { 12 | const stream = fun() 13 | Bluebird.each(arr, v => { 14 | stream.write(v) 15 | }).then(() => { 16 | stream.end() 17 | }) 18 | return fun(Promise.resolve(stream)) 19 | } 20 | 21 | test('identity', t => { 22 | const gen = fromArray([1, 2, 3]) 23 | t.is(Boolean(isaReadable(gen)), true, 'fun-promise: is readable') 24 | t.is(Boolean(isaWritable(gen)), true, 'fun-promise: is writable') 25 | t.is(Boolean(gen.then), true, 'fun-promise: is promise') 26 | t.is(Boolean(FunStream.isFun(gen)), true, 'fun-promise: isFun') 27 | t.done() 28 | }) 29 | 30 | streamTests(test, () => fromArray([1, 2, 3]), { 31 | pipe: {expected: [1, 2, 3]}, 32 | head: {expected: [1, 2]}, 33 | forEach: {expected: [1, 2, 3]}, 34 | filter: {with: [v => v > 1], expected: [2, 3]}, 35 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 36 | flat: {create: () => fromArray([[1, 2], [3, 4]]), expected: [1, 2, 3, 4]}, 37 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 38 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 39 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 40 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 41 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 42 | list: {expected: [1, 2, 3]}, 43 | grab: {create: () => fromArray([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 44 | sort: {create: () => fromArray([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 45 | concat: {expected: ['123'], asyncSkip: true} 46 | }) 47 | 48 | promiseTests(test, () => fromArray([1, 2, 3]), { 49 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 50 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 51 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 52 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 53 | list: {expected: [1, 2, 3]}, 54 | grab: {create: () => fromArray([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 55 | sort: {create: () => fromArray([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 56 | concat: {expected: '123', asyncSkip: true} 57 | }) 58 | -------------------------------------------------------------------------------- /test/fun-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const isaReadable = require('isa-stream').Readable 6 | const isaWritable = require('isa-stream').Writable 7 | const streamTests = require('./lib/interface-tests.js').streamTests 8 | const promiseTests = require('./lib/interface-tests.js').promiseTests 9 | 10 | test('identity', t => { 11 | const arr = fun([1, 2, 3]) 12 | t.is(Boolean(isaReadable(arr)), true, 'fun-array: is readable') 13 | t.is(Boolean(isaWritable(arr)), false, 'fun-array: is not writable') 14 | t.is(Boolean(FunStream.isFun(arr)), true, 'fun-array: isFun') 15 | t.done() 16 | }) 17 | 18 | streamTests(test, () => fun([1, 2, 3]), { 19 | pipe: {expected: [1, 2, 3]}, 20 | head: {expected: [1, 2]}, 21 | forEach: {expected: [1, 2, 3]}, 22 | filter: {with: [v => v > 1], expected: [2, 3]}, 23 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 24 | flat: {create: () => fun([[1, 2], [3, 4]]), expected: [1, 2, 3, 4]}, 25 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 26 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 27 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 28 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 29 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 30 | list: {expected: [1, 2, 3]}, 31 | grab: {create: () => fun([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 32 | sort: {create: () => fun([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 33 | concat: {expected: ['123'], asyncSkip: true} 34 | }) 35 | 36 | // run a second time to catch the "funstream already required" case 37 | streamTests(test, () => fun([1, 2, 3]), { 38 | forEach: {expected: [1, 2, 3]} 39 | }) 40 | 41 | promiseTests(test, () => fun([1, 2, 3]), { 42 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 43 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 44 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 45 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 46 | list: {expected: [1, 2, 3]}, 47 | grab: {create: () => fun([3, 2, 1]), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 48 | sort: {create: () => fun([7, 6, 5]), expected: [5, 6, 7], asyncSkip: true}, 49 | concat: {expected: '123', asyncSkip: true} 50 | }) 51 | 52 | test('backpressure', (t) => { 53 | const astr = fun(new Array(20)) 54 | let lastSeen = (new Date()).valueOf() 55 | let gaps = [] 56 | astr.on('data', data => { 57 | const seen = (new Date()).valueOf() 58 | gaps.push(seen - lastSeen) 59 | lastSeen = seen 60 | }) 61 | astr.map((data, cb) => { 62 | setTimeout(cb, 20, null, '') 63 | return data 64 | }).concat().then(v => { 65 | t.is(v, '', 'Empty map is empty') 66 | const maxGap = Math.max.apply(null, gaps) 67 | t.ok(maxGap > 200, 'Backpressure slowed stream') 68 | t.comment(maxGap) 69 | t.done() 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/fun-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const isaReadable = require('isa-stream').Readable 6 | const isaWritable = require('isa-stream').Writable 7 | const streamTests = require('./lib/interface-tests.js').streamTests 8 | const promiseTests = require('./lib/interface-tests.js').promiseTests 9 | 10 | function * fromArray (arr) { 11 | for (let ii = 0; ii < arr.length; ++ii) { 12 | yield arr[ii] 13 | } 14 | } 15 | 16 | function * fromArrayError (arr) { 17 | for (let ii = 0; ii < arr.length; ++ii) { 18 | if (ii > 0) throw new Error('Boom') 19 | yield ii 20 | } 21 | } 22 | 23 | test('identity', t => { 24 | const gen = fun(fromArray([1, 2, 3])) 25 | t.is(Boolean(isaReadable(gen)), true, 'fun-generator: is readable') 26 | t.is(Boolean(isaWritable(gen)), false, 'fun-generator: is not writable') 27 | t.is(Boolean(FunStream.isFun(gen)), true, 'fun-generator: isFun') 28 | t.done() 29 | }) 30 | 31 | test('backpresure', t => { 32 | const data = [] 33 | for (let ii = 0; ii < 1000; ++ii) { 34 | data.push(ii) 35 | } 36 | const gen = fun(fromArray(data)) 37 | return gen.list().then(result => t.isDeeply(result, data), () => t.fail()) 38 | }) 39 | 40 | test('errors', t => { 41 | const gen = fun(fromArrayError([1, 2, 3])) 42 | return gen.list().then(() => t.fail(), () => t.pass()) 43 | }) 44 | 45 | streamTests(test, () => fun(fromArray([1, 2, 3])), { 46 | pipe: {expected: [1, 2, 3]}, 47 | head: {expected: [1, 2]}, 48 | forEach: {expected: [1, 2, 3]}, 49 | filter: {with: [v => v > 1], expected: [2, 3]}, 50 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 51 | flat: {create: () => fun(fromArray([[1, 2], [3, 4]])), expected: [1, 2, 3, 4]}, 52 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 53 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 54 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 55 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 56 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 57 | list: {expected: [1, 2, 3]}, 58 | grab: {create: () => fun(fromArray([3, 2, 1])), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 59 | sort: {create: () => fun(fromArray([7, 6, 5])), expected: [5, 6, 7], asyncSkip: true}, 60 | concat: {expected: ['123'], asyncSkip: true} 61 | }) 62 | 63 | promiseTests(test, () => fun(fromArray([1, 2, 3])), { 64 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 65 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 66 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 67 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 68 | list: {expected: [1, 2, 3]}, 69 | grab: {create: () => fun(fromArray([3, 2, 1])), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 70 | sort: {create: () => fun(fromArray([7, 6, 5])), expected: [5, 6, 7], asyncSkip: true}, 71 | concat: {expected: '123', asyncSkip: true} 72 | }) 73 | -------------------------------------------------------------------------------- /test/async/fun-async-generator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-disable node/no-unsupported-features/es-syntax */ 3 | const test = require('tap').test 4 | const PassThrough = require('stream').PassThrough 5 | const fun = require('../..') 6 | const FunStream = fun.FunStream 7 | const isaReadable = require('isa-stream').Readable 8 | const isaWritable = require('isa-stream').Writable 9 | const streamTests = require('../lib/interface-tests.js').streamTests 10 | const promiseTests = require('../lib/interface-tests.js').promiseTests 11 | 12 | function runQueue () { 13 | return new Promise(resolve => setImmediate(resolve)) 14 | } 15 | 16 | async function * fromArray (arr) { 17 | for (let ii = 0; ii < arr.length; ++ii) { 18 | yield arr[ii] 19 | } 20 | } 21 | 22 | async function * fromArrayError (arr) { 23 | for (let ii = 0; ii < arr.length; ++ii) { 24 | if (ii > 0) throw new Error('Boom') 25 | yield ii 26 | } 27 | } 28 | 29 | test('identity', t => { 30 | const gen = fun(fromArray([1, 2, 3])) 31 | t.is(Boolean(isaReadable(gen)), true, 'fun-generator: is readable') 32 | t.is(Boolean(isaWritable(gen)), false, 'fun-generator: is not writable') 33 | t.is(Boolean(FunStream.isFun(gen)), true, 'fun-generator: isFun') 34 | t.done() 35 | }) 36 | 37 | test('backpresure', async t => { 38 | const data = [] 39 | for (let ii = 0; ii < 1000; ++ii) { 40 | data.push(ii) 41 | } 42 | const gen = fun(fromArray(data)) 43 | const pt = gen.pipe(new PassThrough({objectMode: true})) 44 | await runQueue() 45 | t.isDeeply(await pt.list(), data) 46 | }) 47 | 48 | test('errors', async t => { 49 | const gen = fun(fromArrayError([1, 2, 3])) 50 | try { 51 | await gen.list() 52 | t.fail() 53 | } catch (_) { 54 | t.pass() 55 | } 56 | }) 57 | 58 | streamTests(test, () => fun(fromArray([1, 2, 3])), { 59 | pipe: {expected: [1, 2, 3]}, 60 | head: {expected: [1, 2]}, 61 | forEach: {expected: [1, 2, 3]}, 62 | filter: {with: [v => v > 1], expected: [2, 3]}, 63 | map: {with: [v => v * 2], expected: [2, 4, 6]}, 64 | flat: {create: () => fun(fromArray([[1, 2], [3, 4]])), expected: [1, 2, 3, 4]}, 65 | flatMap: {with: [v => [v, v]], expected: [1, 1, 2, 2, 3, 3]}, 66 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: [{1: 2, 2: 4, 3: 6}]}, 67 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 68 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: [{result: 6}]}, 69 | reduce: {with: [(acc, v) => acc + v], expected: [6]}, 70 | list: {expected: [1, 2, 3]}, 71 | grab: {create: () => fun(fromArray([3, 2, 1])), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 72 | sort: {create: () => fun(fromArray([7, 6, 5])), expected: [5, 6, 7], asyncSkip: true}, 73 | concat: {expected: ['123'], asyncSkip: true} 74 | }) 75 | 76 | promiseTests(test, () => fun(fromArray([1, 2, 3])), { 77 | reduceToObject: {with: [(acc, v) => { acc[v] = v * 2 }], expected: {1: 2, 2: 4, 3: 6}}, 78 | reduceToArray: {with: [(acc, v) => acc.push(v * 3)], expected: [3, 6, 9]}, 79 | reduceTo: {with: () => [(acc, v) => { acc.result += v }, {result: 0}], expected: {result: 6}}, 80 | reduce: {with: [(acc, v) => acc + v], expected: 6}, 81 | list: {expected: [1, 2, 3]}, 82 | grab: {create: () => fun(fromArray([3, 2, 1])), with: [v => v.sort()], expected: [1, 2, 3], asyncSkip: true}, 83 | sort: {create: () => fun(fromArray([7, 6, 5])), expected: [5, 6, 7], asyncSkip: true}, 84 | concat: {expected: '123', asyncSkip: true} 85 | }) 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = fun 3 | 4 | const is = require('./is.js') 5 | 6 | let FunPassThrough 7 | let FunArray 8 | let FunDuplex 9 | let FunGenerator 10 | let FunAsyncGenerator 11 | let StreamPromise 12 | let mixinPromiseStream 13 | let mixinFun 14 | 15 | Object.defineProperty(fun, 'FunStream', { 16 | enumerable: true, 17 | configurable: true, 18 | get () { 19 | if (!FunPassThrough) FunPassThrough = require('./fun-passthrough.js') 20 | delete fun.FunStream 21 | fun.FunStream = FunPassThrough 22 | return FunPassThrough 23 | } 24 | }) 25 | 26 | try { 27 | /* eslint-disable node/no-unpublished-require */ 28 | fun.Promise = require('bluebird') 29 | } catch (_) { 30 | // we can't repro this till we have npm aliasing 31 | /* istanbul ignore next */ 32 | fun.Promise = Promise 33 | } 34 | 35 | function fun (stream, opts) { 36 | if (stream == null) { 37 | if (!FunPassThrough) FunPassThrough = require('./fun-passthrough.js') 38 | return new FunPassThrough(Object.assign({Promise: fun.Promise}, opts || {})) 39 | } 40 | 41 | if (is.scalar(stream)) { 42 | stream = [stream] 43 | } 44 | if (Array.isArray(stream)) { 45 | if (!FunArray) FunArray = require('./fun-array.js') 46 | return new FunArray(stream, Object.assign({Promise: fun.Promise}, opts || {})) 47 | } 48 | if (typeof stream === 'function') { 49 | if (!FunDuplex) FunDuplex = require('./fun-duplex.js') 50 | const input = fun(null, opts) 51 | const output = stream(input) 52 | return new FunDuplex(input, output, opts) 53 | } 54 | // we actually coverall of the types, so until the standard changes, it's 55 | // impossible to else-out of this if statement. OTOH, you _can_ fall through if 56 | // you run `fun({}, {})`. 57 | /* istanbul ignore else */ 58 | if (typeof stream === 'object') { 59 | if (is.Readable(stream)) { 60 | if (!mixinFun) mixinFun = require('./fun-stream.js').mixin 61 | return mixinFun(stream, Object.assign({Promise: fun.Promise}, opts || {})) 62 | } else if (is.asyncIterator(stream)) { 63 | if (!FunAsyncGenerator) FunAsyncGenerator = require('./fun-async-generator.js') 64 | return new FunAsyncGenerator(stream, Object.assign({Promise: fun.Promise}, opts || {})) 65 | } if (is.iterator(stream)) { 66 | if (!FunGenerator) FunGenerator = require('./fun-generator.js') 67 | return new FunGenerator(stream, Object.assign({Promise: fun.Promise}, opts || {})) 68 | } else if (is.thenable(stream)) { // promises of fun 69 | if (!StreamPromise) StreamPromise = require('./stream-promise.js') 70 | return new StreamPromise(stream, Object.assign({Promise: fun.Promise}, opts || {})) 71 | // note that promise-streamed writables are treated as promises, not as writables 72 | } else if (is.Writable(stream)) { 73 | if (!mixinPromiseStream) mixinPromiseStream = require('./mixin-promise-stream.js') 74 | return mixinPromiseStream(stream, Object.assign({Promise: fun.Promise}, opts || {})) 75 | } else if (opts == null) { 76 | if (!FunPassThrough) FunPassThrough = require('./fun-passthrough.js') 77 | return new FunPassThrough(Object.assign({Promise: fun.Promise}, stream)) 78 | } 79 | } 80 | throw new Error(`funstream invalid arguments, expected: fun([stream | array | scalar], [opts]), got: fun(${[].map.call(arguments, arg => typeof arg).join(', ')})`) 81 | } 82 | fun.with = (todo, opts) => { 83 | const st = fun(opts) 84 | const todoPromise = todo(st) 85 | if (!todoPromise.then) throw new Error('Callback supplied to fun.with did not return a thenable (Promise) as expected.') 86 | todoPromise.then(() => st.end(), err => st.emit('error', err)) 87 | return st 88 | } 89 | -------------------------------------------------------------------------------- /fun-duplex.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const FunStream = require('./fun-stream.js') 3 | const INPUT = Symbol('input') 4 | const OUTPUT = Symbol('output') 5 | 6 | class Duplex { 7 | constructor (input, output, opts) { 8 | this[INPUT] = input 9 | this[OUTPUT] = output 10 | Duplex.funInit.call(this, opts) 11 | } 12 | // EventEmitter 13 | eventNames () { 14 | return this[INPUT].eventNames().concat(this[OUTPUT].eventNames()) 15 | } 16 | addListener (eventName, listener) { 17 | this[INPUT].addListener(eventName, listener) 18 | this[OUTPUT].addListener(eventName, listener) 19 | return this 20 | } 21 | on (eventName, listener) { 22 | this[INPUT].on(eventName, listener) 23 | this[OUTPUT].on(eventName, listener) 24 | return this 25 | } 26 | prependListener (eventName, listener) { 27 | this[INPUT].prependListener(eventName, listener) 28 | this[OUTPUT].prependListener(eventName, listener) 29 | return this 30 | } 31 | removeListener (eventName, listener) { 32 | this[INPUT].removeListener(eventName, listener) 33 | this[OUTPUT].removeListener(eventName, listener) 34 | return this 35 | } 36 | removeAllListeners (eventName, listener) { 37 | this[INPUT].removeAllListeners(eventName) 38 | this[OUTPUT].removeAllListeners(eventName) 39 | return this 40 | } 41 | emit () { 42 | return this[INPUT].emit.apply(this[INPUT], arguments) || 43 | this[OUTPUT].emit.apply(this[OUTPUT], arguments) 44 | } 45 | once (eventName, listener) { 46 | const input = this[INPUT] 47 | const output = this[OUTPUT] 48 | const handler = function () { 49 | if (input !== this) input.removeListener(eventName, handler) 50 | if (output !== this) output.removeListener(eventName, handler) 51 | return listener.apply(this, arguments) 52 | } 53 | input.once(eventName, handler) 54 | output.once(eventName, handler) 55 | } 56 | prependOnceListener (eventName, listener) { 57 | const input = this[INPUT] 58 | const output = this[OUTPUT] 59 | const handler = function () { 60 | if (input !== this) input.removeListener(eventName, handler) 61 | if (output !== this) output.removeListener(eventName, handler) 62 | return listener.apply(this, arguments) 63 | } 64 | input.prependOnceListener(eventName, handler) 65 | output.prependOnceListener(eventName, handler) 66 | } 67 | getMaxListeners () { 68 | return Math.min(this[INPUT].getMaxListeners(), this[OUTPUT].getMaxListeners()) 69 | } 70 | listenerCount (eventName) { 71 | return this[INPUT].listenerCount(eventName) + this[OUTPUT].listenerCount(eventName) 72 | } 73 | listeners (eventName) { 74 | return this[INPUT].listeners(eventName).concat(this[OUTPUT].listeners(eventName)) 75 | } 76 | setMaxListeners (n) { 77 | this[INPUT].setMaxListeners(n) 78 | this[OUTPUT].setMaxListeners(n) 79 | return this 80 | } 81 | // Readable 82 | isPaused () { 83 | return this[OUTPUT].isPaused() 84 | } 85 | pause () { 86 | this[OUTPUT].pause() 87 | return this 88 | } 89 | pipe (dest, opts) { 90 | return this[OUTPUT].pipe(dest, opts) 91 | } 92 | read (size) { 93 | return this[OUTPUT].read(size) 94 | } 95 | resume () { 96 | this[OUTPUT].resume() 97 | return this 98 | } 99 | setEncoding (enc) { 100 | this[OUTPUT].setEncoding(enc) 101 | return this 102 | } 103 | unpipe (dest) { 104 | return this[OUTPUT].unpipe(dest) 105 | } 106 | unshift (chunk) { 107 | return this[OUTPUT].unshift(chunk) 108 | } 109 | // Readable & Writable 110 | destroy (error) { 111 | this[INPUT].destroy(error) 112 | this[OUTPUT].destroy(error) 113 | return this 114 | } 115 | // Writable 116 | cork () { 117 | return this[INPUT].cork() 118 | } 119 | end () { 120 | return this[INPUT].end.apply(this[INPUT], arguments) 121 | } 122 | setDefaultEncoding (encoding) { 123 | this[INPUT].setDefaultEncoding(encoding) 124 | return this 125 | } 126 | uncork () { 127 | return this[INPUT].uncork() 128 | } 129 | write () { 130 | return this[INPUT].write.apply(this[INPUT], arguments) 131 | } 132 | } 133 | FunStream.mixin(Duplex) 134 | 135 | module.exports = Duplex 136 | -------------------------------------------------------------------------------- /test/is.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const is = require('../is.js') 4 | const Readable = require('stream').Readable 5 | const Writable = require('stream').Writable 6 | 7 | function * fromArray (arr) { 8 | for (let ii = 0; ii < arr.length; ++ii) { 9 | yield arr[ii] 10 | } 11 | } 12 | 13 | test('scalar', t => { 14 | t.is(is.scalar(null), true, 'null') 15 | t.is(is.scalar(undefined), true, 'undefined') 16 | t.is(is.scalar(Buffer.alloc(2)), true, 'Buffer') 17 | t.is(is.scalar(''), true, 'String') 18 | t.is(is.scalar(123), true, 'Number') 19 | t.is(is.scalar(Infinity), true, 'Number (Infinity)') 20 | t.is(is.scalar(NaN), true, 'Number (NaN)') 21 | t.is(is.scalar(true), true, 'Boolean') 22 | t.is(is.scalar(Symbol('is')), true, 'Symbol') 23 | t.is(is.scalar([]), false, 'Array') 24 | t.is(is.scalar({}), false, 'Object') 25 | t.is(is.scalar(fromArray()), false, 'Iterator') 26 | t.is(is.scalar(Promise.resolve()), false, 'Promise') 27 | t.is(is.scalar(new Readable()), false, 'Readable') 28 | t.is(is.scalar(new Writable()), false, 'Writable') 29 | t.done() 30 | }) 31 | 32 | test('iterator', t => { 33 | t.is(is.iterator(null), false, 'null') 34 | t.is(is.iterator(undefined), false, 'undefined') 35 | t.is(is.iterator(Buffer.alloc(2)), false, 'Buffer') 36 | t.is(is.iterator(''), false, 'String') 37 | t.is(is.iterator(123), false, 'Number') 38 | t.is(is.iterator(Infinity), false, 'Number (Infinity)') 39 | t.is(is.iterator(NaN), false, 'Number (NaN)') 40 | t.is(is.iterator(false), false, 'Boolean') 41 | t.is(is.iterator(Symbol('is')), false, 'Symbol') 42 | t.is(is.iterator([]), false, 'Array') 43 | t.is(is.iterator({}), false, 'Object') 44 | t.is(is.iterator(fromArray()), true, 'Iterator') 45 | t.is(is.iterator(Promise.resolve()), false, 'Promise') 46 | t.is(is.iterator(new Readable()), false, 'Readable') 47 | t.is(is.iterator(new Writable()), false, 'Writable') 48 | t.done() 49 | }) 50 | test('thenable', t => { 51 | t.is(is.thenable(null), false, 'null') 52 | t.is(is.thenable(undefined), false, 'undefined') 53 | t.is(is.thenable(Buffer.alloc(2)), false, 'Buffer') 54 | t.is(is.thenable(''), false, 'String') 55 | t.is(is.thenable(123), false, 'Number') 56 | t.is(is.thenable(Infinity), false, 'Number (Infinity)') 57 | t.is(is.thenable(NaN), false, 'Number (NaN)') 58 | t.is(is.thenable(false), false, 'Boolean') 59 | t.is(is.thenable(Symbol('is')), false, 'Symbol') 60 | t.is(is.thenable([]), false, 'Array') 61 | t.is(is.thenable({}), false, 'Object') 62 | t.is(is.thenable(fromArray()), false, 'Iterator') 63 | t.is(is.thenable(Promise.resolve()), true, 'Promise') 64 | t.is(is.thenable(new Readable()), false, 'Readable') 65 | t.is(is.thenable(new Writable()), false, 'Writable') 66 | t.done() 67 | }) 68 | test('plainObject', t => { 69 | t.is(is.plainObject(null), false, 'null') 70 | t.is(is.plainObject(undefined), false, 'undefined') 71 | t.is(is.plainObject(Buffer.alloc(2)), false, 'Buffer') 72 | t.is(is.plainObject(''), false, 'String') 73 | t.is(is.plainObject(123), false, 'Number') 74 | t.is(is.plainObject(Infinity), false, 'Number (Infinity)') 75 | t.is(is.plainObject(NaN), false, 'Number (NaN)') 76 | t.is(is.plainObject(false), false, 'Boolean') 77 | t.is(is.plainObject(Symbol('is')), false, 'Symbol') 78 | t.is(is.plainObject([]), false, 'Array') 79 | t.is(is.plainObject({}), true, 'Object') 80 | t.is(is.plainObject(fromArray()), false, 'Iterator') 81 | t.is(is.plainObject(Promise.resolve()), false, 'Promise') 82 | t.is(is.plainObject(new Readable()), false, 'Readable') 83 | t.is(is.plainObject(new Writable()), false, 'Writable') 84 | t.done() 85 | }) 86 | test('asyncIterator', t => { 87 | t.is(is.asyncIterator(null), false, 'null') 88 | t.is(is.asyncIterator(undefined), false, 'undefined') 89 | t.is(is.asyncIterator(Buffer.alloc(2)), false, 'Buffer') 90 | t.is(is.asyncIterator(''), false, 'String') 91 | t.is(is.asyncIterator(123), false, 'Number') 92 | t.is(is.asyncIterator(Infinity), false, 'Number (Infinity)') 93 | t.is(is.asyncIterator(NaN), false, 'Number (NaN)') 94 | t.is(is.asyncIterator(false), false, 'Boolean') 95 | t.is(is.asyncIterator(Symbol('is')), false, 'Symbol') 96 | t.is(is.asyncIterator([]), false, 'Array') 97 | t.is(is.asyncIterator({}), false, 'Object') 98 | t.is(is.asyncIterator(fromArray()), false, 'Iterator') 99 | t.is(is.asyncIterator(Promise.resolve()), false, 'Promise') 100 | t.is(is.asyncIterator(new Readable()), false, 'Readable') 101 | t.is(is.asyncIterator(new Writable()), false, 'Writable') 102 | t.done() 103 | }) 104 | -------------------------------------------------------------------------------- /mixin-promise-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = mixinPromise 3 | const is = require('isa-stream') 4 | 5 | const PROMISE = Symbol('promise') 6 | const CLOSEPROMISE = Symbol('closePromise') 7 | const MAKEPROMISE = Symbol('makePromise') 8 | 9 | mixinPromise.PROMISE = PROMISE 10 | 11 | function mixinPromise (stream, opts) { 12 | const Promise = opts.Promise 13 | const cls = typeof stream === 'function' ? stream : null 14 | const obj = cls ? cls.prototype : stream 15 | 16 | if (MAKEPROMISE in obj) return 17 | 18 | if (!(PROMISE in obj)) { 19 | obj[PROMISE] = null 20 | obj[CLOSEPROMISE] = null 21 | let error 22 | const onError = err => { error = err } 23 | obj.once('error', onError) 24 | if (is.Writable(stream)) { 25 | let result 26 | const onEarlyResult = value => { result = value } 27 | obj.once('result', onEarlyResult) 28 | let finished = false 29 | const onEarlyFinish = () => setImmediate(() => { finished = true }) 30 | obj.once('finish', onEarlyFinish) 31 | obj[MAKEPROMISE] = function () { 32 | if (error) { 33 | this.removeListener('result', onEarlyResult) 34 | this.removeListener('finish', onEarlyFinish) 35 | this.removeListener('close', onEarlyClose) 36 | this[PROMISE] = Promise.reject(error) 37 | } else if (result || finished) { 38 | this.removeListener('error', onError) 39 | this[PROMISE] = Promise.resolve(result) 40 | } else { 41 | this[PROMISE] = new Promise((resolve, reject) => { 42 | this.removeListener('error', onError) 43 | this.removeListener('result', onEarlyResult) 44 | this.removeListener('finish', onEarlyFinish) 45 | this.once('result', resolve) 46 | // make sure finish will always lose any race w/ result 47 | this.once('finish', () => setImmediate(resolve)) 48 | this.once('error', reject) 49 | }) 50 | } 51 | } 52 | let closed = false 53 | const onEarlyClose = () => setImmediate(() => { closed = true }) 54 | obj.once('close', onEarlyClose) 55 | obj.fun$closed = function () { 56 | if (!is.Writable(this)) throw new TypeError('This stream is not a writable stream, it will not close. Try `.ended()` instead.') 57 | if (this[CLOSEPROMISE]) return this[CLOSEPROMISE] 58 | if (error) { 59 | this.removeListener('close', onEarlyFinish) 60 | return this 61 | } 62 | if (closed) return this[CLOSEPROMISE] = Promise.resolve() 63 | 64 | return this[CLOSEPROMISE] = new Promise((resolve, reject) => { 65 | this.removeListener('close', onEarlyFinish) 66 | this.once('error', reject) 67 | this.once('close', resolve) 68 | }) 69 | } 70 | } else { 71 | let ended = false 72 | let result 73 | const onEarlyResult = value => { result = value } 74 | obj.once('result', onEarlyResult) 75 | const onEarlyEnd = () => { ended = true } 76 | obj.once('end', onEarlyEnd) 77 | obj[MAKEPROMISE] = function () { 78 | if (error) { 79 | this.removeListener('result', onEarlyResult) 80 | this.removeListener('end', onEarlyEnd) 81 | this[PROMISE] = Promise.reject(error) 82 | } else if (result || ended) { 83 | this.removeListener('error', onError) 84 | this[PROMISE] = Promise.resolve() 85 | } else { 86 | this.removeListener('result', onEarlyResult) 87 | this.removeListener('end', onEarlyEnd) 88 | this.removeListener('error', onError) 89 | this[PROMISE] = new Promise((resolve, reject) => { 90 | this.once('result', resolve) 91 | this.once('end', () => setImmediate(resolve)) 92 | this.once('error', reject) 93 | }) 94 | } 95 | } 96 | } 97 | } 98 | 99 | // the fun-stream ways of requesting a promise are passthroughs 100 | const nop = function () { return this } 101 | 102 | if (is.Writable(obj)) { 103 | obj.fun$finished = nop 104 | } else { 105 | obj.fun$ended = nop 106 | } 107 | 108 | // the core interface 109 | for (let name of ['then', 'catch']) { 110 | let func = obj[PROMISE] ? obj[PROMISE][name] : Promise.prototype[name] 111 | obj[name] = makeProxy(func) 112 | } 113 | // and everything else, iterating prototype doesn't 114 | // work on builtin promises, thus the hard coded list above. 115 | const methods = obj[PROMISE] 116 | ? Object.keys(Object.getPrototypeOf(obj[PROMISE])).concat(Object.keys(obj[PROMISE])) 117 | : Object.keys(Promise.prototype) 118 | methods.forEach(name => { 119 | if (name[0] === '_') return 120 | if (name in obj) return 121 | let func = Promise.prototype[name] 122 | if (typeof func !== 'function') return 123 | obj[name] = makeProxy(func) 124 | }) 125 | return stream 126 | } 127 | 128 | function makeProxy (func) { 129 | return function () { 130 | if (!this[PROMISE]) this[MAKEPROMISE]() 131 | return func.apply(this[PROMISE], arguments) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/construction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tap').test 3 | const fun = require('..') 4 | const FunStream = fun.FunStream 5 | const stream = require('stream') 6 | const is = require('../is.js') 7 | 8 | test('construction', t => { 9 | const resolutions = [] 10 | function hasValue (stream, value, label) { 11 | resolutions.push(stream.concat().then(v => t.is(v, value, `${label} has expected value`))) 12 | } 13 | function isReadable (make, value, label) { 14 | const result = make() 15 | t.is(FunStream.isFun(result), true, `${label} is fun`) 16 | t.is(is.Readable(result), true, `${label} is readable`) 17 | t.is(is.Writable(result), false, `${label} IS NOT writable`) 18 | hasValue(result, value, label) 19 | } 20 | function isWritable (make, label) { 21 | const result = make() 22 | t.is(FunStream.isFun(result), false, `${label} is NOT fun`) 23 | t.is(is.Readable(result), false, `${label} IS NOT readable`) 24 | t.is(is.Writable(result), true, `${label} is writable`) 25 | } 26 | function isDuplex (make, value, label) { 27 | const result = make() 28 | t.is(FunStream.isFun(result), true, `${label} is fun`) 29 | t.is(is.Duplex(result), true, `${label} is duplex`) 30 | hasValue(result, value, label) 31 | } 32 | function funAndEnd () { 33 | const stream = fun.apply(null, arguments) 34 | stream.end() 35 | return stream 36 | } 37 | 38 | isDuplex(() => funAndEnd(), '', 'fun()') 39 | isDuplex(() => funAndEnd({async: true}), '', 'fun(opts)') 40 | isDuplex(() => funAndEnd(null, {async: true}), '', 'fun(null, opts)') 41 | isReadable(() => fun([1, 2, 3]), '123', 'fun(array)') 42 | isReadable(() => fun('abc'), 'abc', 'fun(string)') 43 | isReadable(() => fun(Buffer.from('abc')), 'abc', 'fun(buffer)') 44 | isReadable(() => fun(123), '123', 'fun(number)') 45 | isReadable(() => fun(false), 'false', 'fun(boolean)') 46 | isReadable(() => fun(Symbol('abc')), 'Symbol(abc)', 'fun(symbol)') 47 | 48 | function * mygen () { 49 | for (let ii = 0; ii < 3; ++ii) { 50 | yield ii 51 | } 52 | } 53 | isReadable(() => fun(mygen()), '012', 'fun(iterator)') 54 | isReadable(() => fun(new stream.Readable({read () { this.push('hi'); this.push(null) }})), 'hi', 'fun(Readable)') 55 | isWritable(() => fun(new stream.Writable({write () { return true }})), 'fun(Writable)') 56 | 57 | t.throws(() => { 58 | fun({}, {}) 59 | }, /invalid arguments/, "fun won't fun just anything") 60 | return Promise.all(resolutions) 61 | }) 62 | 63 | test('promised construction', t => { 64 | const resolutions = [] 65 | function hasValue (stream, value, label) { 66 | resolutions.push(stream.concat().then(v => t.is(v, value, `Promise ${label} has expected value`))) 67 | } 68 | function assertFun (make, value, label) { 69 | const result = make() 70 | t.is(FunStream.isFun(result), true, `Promise ${label} is fun`) 71 | t.is(is.Readable(result), true, `Promise ${label} is readable`) 72 | t.is(is.Writable(result), true, `Promise ${label} is writable`) 73 | hasValue(result, value, label) 74 | } 75 | function funAndEnd () { 76 | const stream = fun.apply(null, arguments) 77 | stream.end() 78 | return stream 79 | } 80 | 81 | assertFun(() => funAndEnd(Promise.resolve()), '', 'fun()') 82 | assertFun(() => funAndEnd(Promise.resolve(), {async: true}), '', 'fun(promise, opts)') 83 | assertFun(() => fun(Promise.resolve([1, 2, 3])), '123', 'fun(array)') 84 | assertFun(() => fun(Promise.resolve('abc')), 'abc', 'fun(string)') 85 | assertFun(() => fun(Promise.resolve(Buffer.from('abc'))), 'abc', 'fun(buffer)') 86 | assertFun(() => fun(Promise.resolve(123)), '123', 'fun(number)') 87 | assertFun(() => fun(Promise.resolve(false)), 'false', 'fun(boolean)') 88 | assertFun(() => fun(Promise.resolve(Symbol('abc'))), 'Symbol(abc)', 'fun(symbol)') 89 | 90 | function * mygen () { 91 | for (let ii = 0; ii < 3; ++ii) { 92 | yield ii 93 | } 94 | } 95 | assertFun(() => fun(Promise.resolve(mygen())), '012', 'fun(iterator)') 96 | assertFun(() => fun(Promise.resolve(new stream.Readable({read () { this.push('hi'); this.push(null) }}))), 'hi', 'fun(Readable)') 97 | 98 | const writableFun = fun(Promise.resolve(new stream.Writable({write () { return true }}))) 99 | t.is(FunStream.isFun(writableFun), true, "Promise fun(Writable) is fun (because we don't know any better)") 100 | t.is(typeof writableFun.pause, 'function', "Promise fun(Writable) is readable (because we don't know any better)") 101 | t.is(typeof writableFun.write, 'function', 'Promise fun(Writable) is writable') 102 | t.is(typeof writableFun.then, 'function', 'Promise fun(Writable) IS thenable (because everything always is)') 103 | 104 | const value = {} 105 | const rejectedFun = fun(Promise.resolve(value)) 106 | rejectedFun.on('data', v => t.is(v, value, 'Promise object was streamed through verbatum')) 107 | resolutions.push(new Promise(resolve => { 108 | rejectedFun.on('error', err => { 109 | t.ifError(err, 'Promise objects are streamed w/o errors') 110 | resolve() 111 | }) 112 | rejectedFun.on('finish', () => { 113 | t.pass('Promise objects are streamed w/o errors') 114 | resolve() 115 | }) 116 | })) 117 | 118 | return Promise.all(resolutions) 119 | }) 120 | -------------------------------------------------------------------------------- /stream-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fun = require('./index.js') 3 | const FunStream = require('./fun-stream.js') 4 | const PROMISE = require('./mixin-promise-stream.js').PROMISE 5 | const STREAM = Symbol('stream') 6 | const MAKEME = Symbol('makeme') 7 | const PIPES = Symbol('pipes') 8 | const UNPIPE = Symbol('unpipe') 9 | const OPTS = FunStream.OPTS 10 | const PassThrough = require('stream').PassThrough 11 | const mixinPromiseStream = require('./mixin-promise-stream') 12 | const is = require('./is.js') 13 | 14 | // this is basically the opposite of the normal stream support, we START with a promise and 15 | // only lazily construct a stream 16 | class StreamPromise extends FunStream { 17 | constructor (promise, opts) { 18 | super() 19 | this[PROMISE] = promise 20 | const P = Object.getPrototypeOf(promise) === Promise.prototype ? Promise : opts.Promise 21 | mixinPromiseStream(this, {Promise: P}) 22 | this.init(this, opts) 23 | this[PIPES] = new Map() 24 | this[UNPIPE] = [] 25 | } 26 | 27 | [MAKEME] () { 28 | this[STREAM] = new PassThrough(Object.assign({objectMode: true}, this[OPTS])) 29 | this[PROMISE].then(promised => { 30 | const srcStream = fun(is.plainObject(promised) ? [promised] : promised) 31 | if (promised == null) srcStream.end() 32 | return StreamPromise.isFun(srcStream) ? srcStream.pipe(this) : this.pipe(srcStream) 33 | }).catch(err => this.emit('error', err)) 34 | } 35 | 36 | fun$ended () { 37 | return this 38 | } 39 | 40 | // EventEmitter 41 | eventNames () { 42 | if (!this[STREAM]) this[MAKEME]() 43 | return this[STREAM].eventNames() 44 | } 45 | addListener (eventName, listener) { 46 | if (!this[STREAM]) this[MAKEME]() 47 | this[STREAM].addListener(eventName, listener) 48 | return this 49 | } 50 | on (eventName, listener) { 51 | if (!this[STREAM]) this[MAKEME]() 52 | this[STREAM].on(eventName, listener) 53 | return this 54 | } 55 | prependListener (eventName, listener) { 56 | if (!this[STREAM]) this[MAKEME]() 57 | this[STREAM].prependListener(eventName, listener) 58 | return this 59 | } 60 | removeListener (eventName, listener) { 61 | if (!this[STREAM]) this[MAKEME]() 62 | this[STREAM].removeListener(eventName, listener) 63 | return this 64 | } 65 | removeAllListeners (eventName, listener) { 66 | if (!this[STREAM]) this[MAKEME]() 67 | this[STREAM].removeAllListeners(eventName) 68 | return this 69 | } 70 | emit () { 71 | if (!this[STREAM]) this[MAKEME]() 72 | return this[STREAM].emit.apply(this[STREAM], arguments) 73 | } 74 | once (eventName, listener) { 75 | if (!this[STREAM]) this[MAKEME]() 76 | const input = this[STREAM] 77 | const handler = function () { 78 | if (input !== this) input.removeListener(eventName, handler) 79 | return listener.apply(this, arguments) 80 | } 81 | input.once(eventName, handler) 82 | } 83 | prependOnceListener (eventName, listener) { 84 | if (!this[STREAM]) this[MAKEME]() 85 | const input = this[STREAM] 86 | const handler = function () { 87 | if (input !== this) input.removeListener(eventName, handler) 88 | return listener.apply(this, arguments) 89 | } 90 | input.prependOnceListener(eventName, handler) 91 | } 92 | getMaxListeners () { 93 | if (!this[STREAM]) this[MAKEME]() 94 | return this[STREAM].getMaxListeners() 95 | } 96 | listenerCount (eventName) { 97 | if (!this[STREAM]) this[MAKEME]() 98 | return this[STREAM].listenerCount(eventName) 99 | } 100 | listeners (eventName) { 101 | if (!this[STREAM]) this[MAKEME]() 102 | return this[STREAM].listeners(eventName) 103 | } 104 | setMaxListeners (n) { 105 | if (!this[STREAM]) this[MAKEME]() 106 | this[STREAM].setMaxListeners(n) 107 | return this 108 | } 109 | // Readable 110 | isPaused () { 111 | if (!this[STREAM]) this[MAKEME]() 112 | return this[STREAM].isPaused() 113 | } 114 | pause () { 115 | if (!this[STREAM]) this[MAKEME]() 116 | this[STREAM].pause() 117 | return this 118 | } 119 | pipe (dest, opts) { 120 | if (!this[STREAM]) this[MAKEME]() 121 | const forwardError = err => { 122 | if (err.src === undefined) err.src = this 123 | dest.emit('error', err) 124 | } 125 | this.on('error', forwardError) 126 | const wrapped = fun(this[STREAM].pipe(dest, opts), this[OPTS]) 127 | this[UNPIPE].push([wrapped, dest]) 128 | this[PIPES].set(dest, forwardError) 129 | return wrapped 130 | } 131 | read (size) { 132 | if (!this[STREAM]) this[MAKEME]() 133 | return this[STREAM].read(size) 134 | } 135 | resume () { 136 | if (!this[STREAM]) this[MAKEME]() 137 | this[STREAM].resume() 138 | return this 139 | } 140 | setEncoding (enc) { 141 | if (!this[STREAM]) this[MAKEME]() 142 | this[STREAM].setEncoding(enc) 143 | return this 144 | } 145 | unpipe (dest) { 146 | if (!this[STREAM]) this[MAKEME]() 147 | const real = this[UNPIPE].filter(p => p[0] === dest)[0] 148 | if (real) dest = real[1] 149 | this[STREAM].unpipe(dest) 150 | const pipes = dest ? [dest] : this[PIPES].keys() 151 | pipes.forEach(pipe => { 152 | const real = this[UNPIPE].filter(p => p[0] === pipe)[0] 153 | const dest = real ? real[1] : pipe 154 | this[UNPIPE] = this[UNPIPE].filter(p => p[0] !== dest && p[1] !== dest) 155 | const listener = this[PIPES].get(dest) 156 | if (listener) this[STREAM].removeListener('error', listener) 157 | this[PIPES].delete(dest) 158 | }) 159 | } 160 | unshift (chunk) { 161 | if (!this[STREAM]) this[MAKEME]() 162 | return this[STREAM].unshift(chunk) 163 | } 164 | // Readable & Writable 165 | destroy (error) { 166 | if (!this[STREAM]) this[MAKEME]() 167 | this[STREAM].destroy(error) 168 | this[STREAM].destroy(error) 169 | return this 170 | } 171 | // Writable 172 | cork () { 173 | if (!this[STREAM]) this[MAKEME]() 174 | return this[STREAM].cork() 175 | } 176 | end () { 177 | if (!this[STREAM]) this[MAKEME]() 178 | return this[STREAM].end.apply(this[STREAM], arguments) 179 | } 180 | setDefaultEncoding (encoding) { 181 | if (!this[STREAM]) this[MAKEME]() 182 | this[STREAM].setDefaultEncoding(encoding) 183 | return this 184 | } 185 | uncork () { 186 | if (!this[STREAM]) this[MAKEME]() 187 | return this[STREAM].uncork() 188 | } 189 | write () { 190 | if (!this[STREAM]) this[MAKEME]() 191 | return this[STREAM].write.apply(this[STREAM], arguments) 192 | } 193 | } 194 | // inherit the class methods too 195 | Object.keys(FunStream).forEach(k => { StreamPromise[k] = FunStream[k] }) 196 | 197 | module.exports = StreamPromise 198 | -------------------------------------------------------------------------------- /test/lib/interface-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | exports.streamTests = streamTests 3 | exports.promiseTests = promiseTests 4 | 5 | const FunStream = require('../..').FunStream 6 | const PassThrough = require('stream').PassThrough 7 | const qw = require('qw') 8 | 9 | const testCases = qw` 10 | filter map flat flatMap reduceToObject reduceToArray reduce reduceTo list 11 | grab sort concat` 12 | 13 | function streamTests (test, create, results) { 14 | test('stream', t => { 15 | if (results.pipe) { 16 | t.test('pipe', t => { 17 | const target = new PassThrough({objectMode: true}) 18 | const testcase = create().pipe(target) 19 | return streamContent(testcase).then(content => { 20 | t.is(Boolean(target.then), true, 'target stream was promisified') 21 | t.is(Boolean(FunStream.isFun(target)), true, 'target stream was funified') 22 | t.isDeeply(content, results.pipe.expected, 'target stream content is correct') 23 | }) 24 | }) 25 | } 26 | if (results.head) { 27 | t.test('head', t => { 28 | const testcase = create().head(2) 29 | return streamContent(testcase).then(content => { 30 | t.isDeeply(content, results.head.expected, 'target stream content is correct') 31 | }) 32 | }) 33 | } 34 | if (results.forEach) { 35 | t.test('forEach (sync)', t => { 36 | const calledWith = [] 37 | return create().forEach(v => { 38 | calledWith.push(v) 39 | }).then(() => { 40 | t.isDeeply(calledWith, results.forEach.expected, 'foreach was called with each value') 41 | }) 42 | }) 43 | } 44 | t.test('forEach (async)', t => { 45 | const calledWith = [] 46 | return create().forEach((v, cb) => { 47 | calledWith.push(v) 48 | cb() 49 | }).then(() => { 50 | t.isDeeply(calledWith, results.forEach.expected, 'foreach was called with each value') 51 | }) 52 | }) 53 | 54 | runTestGroup('sync', g => { 55 | testCases.forEach(fn => g.maybe(fn, t => { 56 | const createWith = results[fn].create || create 57 | let args = results[fn].with || [] 58 | if (typeof args === 'function') args = args() 59 | const stream = createWith() 60 | const testcase = stream[fn].apply(stream, args) 61 | return streamContent(testcase).then(content => { 62 | t.isDeeply(content, results[fn].expected, `${fn}: stream ok`) 63 | }) 64 | })) 65 | }) 66 | 67 | runTestGroup('async', g => { 68 | testCases.forEach(fn => g.maybe(fn, t => { 69 | const createWith = results[fn].create || create 70 | let args 71 | if (results[fn].withAsync) { 72 | args = results[fn].withAsync 73 | if (typeof args === 'function') args = args() 74 | } else { 75 | args = results[fn].with || [] 76 | if (typeof args === 'function') args = args() 77 | if (typeof args[0] === 'function') { 78 | const tw = args[0] 79 | // our wrappers have to have an arity to autodetect as async 80 | if (tw.length === 1) { 81 | args[0] = function (v, cb) { 82 | return cb(null, tw(v)) 83 | } 84 | } else if (tw.length === 2) { 85 | args[0] = function (v1, v2, cb) { 86 | return cb(null, tw(v1, v2)) 87 | } 88 | } else { 89 | throw new Error('Unsupported arity: ' + tw.length) 90 | } 91 | } 92 | } 93 | const stream = createWith() 94 | const testcase = stream[fn].apply(stream, args) 95 | return streamContent(testcase).then(content => { 96 | t.isDeeply(content, results[fn].expected, `${fn}: stream ok`) 97 | }) 98 | })) 99 | }) 100 | function runTestGroup (name, todo) { 101 | let tg = new TestGroup(name, results) 102 | t.test(name, t => { 103 | tg.setT(t) 104 | todo.call(tg, tg) 105 | return tg.done() 106 | }) 107 | } 108 | t.done() 109 | }) 110 | } 111 | 112 | function streamContent (stream) { 113 | return new Promise((resolve, reject) => { 114 | stream.on('error', reject) 115 | const content = [] 116 | stream.on('data', v => content.push(v)) 117 | stream.on('end', () => resolve(content)) 118 | }) 119 | } 120 | 121 | function promiseTests (test, create, results) { 122 | test('promise', t => { 123 | runTestGroup('sync', g => { 124 | testCases.forEach(fn => g.maybe(fn, t => { 125 | const createWith = results[fn].create || create 126 | let args = results[fn].with || [] 127 | if (typeof args === 'function') args = args() 128 | const stream = createWith() 129 | const testcase = stream[fn].apply(stream, args) 130 | return testcase.then(content => { 131 | t.isDeeply(content, results[fn].expected, `${fn}: stream ok`) 132 | }) 133 | })) 134 | }) 135 | 136 | runTestGroup('async', g => { 137 | testCases.forEach(fn => g.maybe(fn, t => { 138 | const createWith = results[fn].create || create 139 | let args 140 | if (results[fn].withAsync) { 141 | args = results[fn].withAsync 142 | if (typeof args === 'function') args = args() 143 | } else { 144 | args = results[fn].with || [] 145 | if (typeof args === 'function') args = args() 146 | if (typeof args[0] === 'function') { 147 | const tw = args[0] 148 | // our wrappers have to have an arity to autodetect as async 149 | if (tw.length === 1) { 150 | args[0] = function (v, cb) { 151 | return cb(null, tw(v)) 152 | } 153 | } else if (tw.length === 2) { 154 | args[0] = function (v1, v2, cb) { 155 | return cb(null, tw(v1, v2)) 156 | } 157 | } else { 158 | throw new Error('Unsupported arity: ' + tw.length) 159 | } 160 | } 161 | } 162 | const stream = createWith() 163 | const testcase = stream[fn].apply(stream, args) 164 | return testcase.then(content => { 165 | t.isDeeply(content, results[fn].expected, `${fn}: stream ok`) 166 | }) 167 | })) 168 | }) 169 | function runTestGroup (name, todo) { 170 | let tg = new TestGroup(name, results) 171 | t.test(name, t => { 172 | tg.setT(t) 173 | todo.call(tg, tg) 174 | return tg.done() 175 | }) 176 | } 177 | t.done() 178 | }) 179 | } 180 | 181 | class TestGroup { 182 | constructor (type, results) { 183 | this.type = type 184 | this.results = results 185 | this.waitingOn = [] 186 | this.t = null 187 | } 188 | setT (t) { 189 | this.t = t 190 | } 191 | maybe (fn, todo) { 192 | const skip = this.type + 'Skip' 193 | if (!this.results[fn] || this.results[fn][skip]) { 194 | return 195 | } 196 | this.waitingOn.push(todo(this.t)) 197 | } 198 | done () { 199 | return Promise.all(this.waitingOn) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.2.0 2 | 3 | * Add `stream.fun.writable()` to get a promisey way of asserting a stream is ready for data 4 | * Add `stream.toJson()` which takes a series of objects and emits them as a JSON stringified array. 5 | * Add `stream.toNdjson()` which takes a series of objects and emits them as new-line delimited JSON. 6 | * Add `stream.fromJson()` which is the same as `stream.json()` 7 | * Add `stream.fromNdjson()` which is the same as `stream.ndjson()` 8 | * Fixed `fun.finished` and `fun.ended`, which previously would refuse to run. 9 | 10 | # 4.1.1 11 | 12 | * Fix `stream.lines()` (and in turn `stream.ndjson()`) to support CRLF terminated lines. 13 | * Fix a bug where if multiple chunks were emitted without newlines end them and then the stream closed, they would be emitted with newlines injected between them (but as a single "line"). 14 | 15 | # 4.1.0 16 | 17 | * Add support for `stream.lines()` to split the stream on lines and emit one line at a time. 18 | * Add support for `stream.ndjson()` to parse the stream as newline delimited JSON and emit one object at a time. 19 | 20 | # 4.0.0 21 | 22 | * Our recently introduced closed/finished/ended methods stomped on some stream implementation's use of them 23 | as properties. Change the API to put them in a safer place. 24 | 25 | # 3.2.0 26 | 27 | * Fix crash when a stream object's pipe method was readonly. (For example, with minipass.) 28 | * Improve stream ducktyping to support minipass based streams. 29 | * Fix error in reduceTo family of functions when promise returning reductions, where we were running 30 | all the reductions in parallel rather than waiting for them to complete individually. 31 | * Guard against crashes when promises are rejected with null/undefined values. 32 | * Improve error messaging on invalid arguments. 33 | * Add support for `fun(123n)` working like `fun(123)` 34 | 35 | # 3.1.0 36 | 37 | * Fix bug where, when mixing in promise-streams (when upgrading a regular 38 | stream to a funstream), the provision of ended versus finished methods was 39 | swapped. 40 | * Add support for async generators (thank you Node 10!!) 41 | * Add `stream.mutate()`, which acts like `stream.map()` but doesn't do anything with the return value of the 42 | callback. `stream.mutate(_ => { ... })` is equiv of `stream.map(_ => { ...; return _})` 43 | * Add `stream.collect()` as an alias for `stream.list()` 44 | * Add `fun.with(cb)` constructor that passes the callback a stream that's ended when the promise 45 | returned by the callback is resolved. This makes manual writing to a stream far more convenient. 46 | * Add `stream.json()` convenience method, equiv of `stream.concat().then(_ => JSON.parse(_))` 47 | * Fix error propagation in `stream.reduce()`. 48 | * Guard iterator/thenable type checking when given symbols. This would 49 | caused a crash if you constructed a funstream from a Symbol. Weird, but 50 | shouldn't crash. 51 | 52 | # 3.0.0 53 | 54 | I'm doing a 3.0.0 to backout the feature added in 2.3.0 where Readable 55 | streams were thenable. 56 | 57 | This turns out to be very unfun. Specifically it means that async functions 58 | can't return fun-streams, something I very much want to do. 59 | 60 | Because it's still sometimes nice to get a promise in these circumstances, 61 | you can ask for one with a few ways: 62 | 63 | `.finished()` is available on writable and duplex streams and resolves when the stream 64 | emits `finish`. Is is a no-op on writable streams, as they already are a 65 | promise that resolves when the stream emits `finish`. 66 | 67 | `.ended()` is available on readable and duplex streams and resolves when the 68 | stream emits `end`. 69 | 70 | `.closed()` is available on writable and duplex streams and resolves when the 71 | stream emits `close`. Note that not all streams emit `close` and if this is 72 | one of those then the promise will never resolve. 73 | 74 | # 2.6.1 75 | 76 | * Revert: Patch to preserve funopts confuses itself. 77 | 78 | # 2.6.0 79 | 80 | * Feature: Lazily convert promises to streams. If you funify a promise and 81 | use as a promise, no stream infrastructure will be created. (This is 82 | important for promise-returning fun functions.) 83 | * Feature: Funify writable promise-streams as if they were promises. This 84 | means that if they a value that value will be preserved, and that means 85 | that `fun().reduce().forEach()` will work, for instance. 86 | * Feature: `forEach` on array-streams now has a synchronous fast-path 87 | implementation, where we ignore the stream entirely and just loop over the 88 | in memory array. 89 | * Tests: Many, MANY, were added. A number of files are at 100% coverage now. 90 | * Fix: Async reduce streams previously would finish early and given incomplete results. 91 | * Fix: The async reduceTo impelementation previously did not work at all. 92 | * Fix: Reduce streams weren't copying along options from their parents. 93 | This only matters if you chain off of them as a fun-stream and not a 94 | promise. 95 | * Fix: `.concat()` will now work with streams w/ elements that can't be added to strings. (eg, Symbols) 96 | * Fix: If you pass an existing readable fun-stream to fun we used to just 97 | return it. We still do that if you provide the same options as the 98 | fun-stream, if you didn't we pipe into a new one with your options. 99 | * Fix: `.grab()` now returns a promise-stream. 100 | * Fix: Sugar like `.concat()` would previously fail if you'd explictly set the mode to `.async()`. This is now fixed. 101 | 102 | # 2.5.1 103 | 104 | * Readme improvements 105 | 106 | # 2.5.0 107 | 108 | * Strings can now be fun too. 109 | * Also Buffers. 110 | * Improve stream duck typing 111 | * Really, truely, all things are _also_ promises now. 112 | 113 | # 2.4.0 114 | 115 | * Add pipe-chain bundling in the form of `fun(stream => stream.pipe.chain)`. 116 | 117 | # 2.3.2 118 | 119 | * Fix async flatMap 120 | 121 | # 2.3.1 122 | 123 | * Eep actually include flat-map-stream in the artifact 124 | 125 | # 2.3.0 126 | 127 | New features! 128 | 129 | * All streams are promises: `fun(mystream).then(…)` will wait wait for your 130 | stream to `end` in the case of read streams or `finish` in the case of 131 | write and duplex streams. 132 | There's no overhead to this: No promise is constructed if you don't call 133 | a promise method on the resulting objects. 134 | If you want a Bluebird (or other, non-native) promise implementation you 135 | can pass one in as an option `fun(mystream, {Promise: require('bluebird'})` 136 | * flat: emit each object of an array as a new element of the stream 137 | * flatMap: transform the input and apply flat as above to the output 138 | * list: sugar for turning the stream into an array 139 | * grab: opperate on the entire stream as an array while still chaining back 140 | into a stream 141 | * concat: returns the entire stream as a single string 142 | 143 | Improvements! 144 | 145 | * forEach: sync calls have a fast-path now 146 | * All module loading that can be lazy, now is lazy, for faster loads and 147 | less memory use. 148 | 149 | Bug fixes! 150 | 151 | * The async stream reducer was entirely broken. Now fixed. 152 | 153 | 154 | # 2.2.0 155 | 156 | * Writable streams are now fun! Fun a writable stream and get a 157 | promise/stream hybrid. The promise part will resolve when the stream 158 | finishes, or rejects when it errors. The stream part is still a perfectly 159 | ordinary write stream with all the usual features. 160 | * Errors aren't fun, but they now at least cop to who was to blame for them in `error.src`. 161 | * Generators can be fun! Fun a generator to get a fun stream of values. 162 | 163 | * Bug fix: `forEach` fun now gets option choices and those your choice of promises. 164 | * More symbols are more fun. isFun property is now Symbol(ISFUN) and no longer polutes your namespaces. 165 | -------------------------------------------------------------------------------- /bench/map.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fun = require('../') 3 | const stream = require('stream') 4 | const through2 = require('through2') 5 | 6 | class NullSink extends stream.Writable { 7 | constructor () { 8 | super({objectMode: true}) 9 | } 10 | _write (data, encoding, next) { 11 | if (next) next() 12 | return true 13 | } 14 | } 15 | 16 | const data = new Array(1000) 17 | let acc = '' 18 | for (let ii = 0; ii < 1000; ++ii) { 19 | data.push(acc += ii) 20 | } 21 | acc = null 22 | 23 | class Numbers extends stream.Readable { 24 | constructor () { 25 | super({objectMode: true}) 26 | this.ii = 0 27 | this.acc = '' 28 | } 29 | _read () { 30 | let flowing = true 31 | while (flowing) { 32 | if (++this.ii >= 1000) { 33 | return this.push(null) 34 | } else { 35 | flowing = this.push(this.acc += this.ii) 36 | } 37 | } 38 | } 39 | } 40 | module.exports = suite => { 41 | suite.add('stream.Transform 1', { 42 | defer: true, 43 | fn (deferred) { 44 | const out = new NullSink() 45 | out.on('finish', () => deferred.resolve()) 46 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 47 | new Numbers().pipe(new stream.Transform({ 48 | objectMode: true, 49 | transform (data, enc, cb) { 50 | this.push(data + 1) 51 | cb() 52 | } 53 | })).pipe(out) 54 | } 55 | }) 56 | suite.add('through2 1', { 57 | defer: true, 58 | fn (deferred) { 59 | const out = new NullSink() 60 | out.on('finish', () => deferred.resolve()) 61 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 62 | new Numbers().pipe(through2.obj(function (data, enc, cb) { 63 | this.push(data + 1) 64 | cb() 65 | })).pipe(out) 66 | } 67 | }) 68 | suite.add('fun-sync 1', { 69 | defer: true, 70 | fn (deferred) { 71 | const out = new NullSink() 72 | out.on('finish', () => deferred.resolve()) 73 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 74 | fun(new Numbers()).map(n => n + 1).pipe(out) 75 | } 76 | }) 77 | 78 | suite.add('fun-async 1', { 79 | defer: true, 80 | fn (deferred) { 81 | const out = new NullSink() 82 | out.on('finish', () => deferred.resolve()) 83 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 84 | fun(new Numbers()).async().map(async n => await n + 1).pipe(out) 85 | } 86 | }) 87 | suite.add('fun-cb 1', { 88 | defer: true, 89 | fn (deferred) { 90 | const out = new NullSink() 91 | out.on('finish', () => deferred.resolve()) 92 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 93 | fun(new Numbers()).async().map((n, cb) => cb(null, n + 1)).pipe(out) 94 | } 95 | }) 96 | suite.add('fun-promise 1', { 97 | defer: true, 98 | fn (deferred) { 99 | const out = new NullSink() 100 | out.on('finish', () => deferred.resolve()) 101 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 102 | fun(new Numbers()).async().map(n => Promise.resolve(n + 1)).pipe(out) 103 | } 104 | }) 105 | 106 | suite.add('stream.Transform', { 107 | defer: true, 108 | fn (deferred) { 109 | const out = new NullSink() 110 | out.on('finish', () => deferred.resolve()) 111 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 112 | new Numbers().pipe(new stream.Transform({ 113 | objectMode: true, 114 | transform (data, enc, cb) { 115 | this.push(data + 1) 116 | cb() 117 | } 118 | })).pipe(new stream.Transform({ 119 | objectMode: true, 120 | transform (data, enc, cb) { 121 | this.push(data % 2) 122 | cb() 123 | } 124 | })).pipe(new stream.Transform({ 125 | objectMode: true, 126 | transform (data, enc, cb) { 127 | this.push(`${data}\n`) 128 | cb() 129 | } 130 | })).pipe(out) 131 | } 132 | }) 133 | suite.add('through2', { 134 | defer: true, 135 | fn (deferred) { 136 | const out = new NullSink() 137 | out.on('finish', () => deferred.resolve()) 138 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 139 | new Numbers().pipe(through2.obj(function (data, enc, cb) { 140 | this.push(data + 1) 141 | cb() 142 | })).pipe(through2.obj(function (data, enc, cb) { 143 | this.push(data % 2) 144 | cb() 145 | })).pipe(through2.obj(function (data, enc, cb) { 146 | this.push(`${data}\n`) 147 | cb() 148 | })).pipe(out) 149 | } 150 | }) 151 | 152 | suite.add('fun-sync', { 153 | defer: true, 154 | fn (deferred) { 155 | const out = new NullSink() 156 | out.on('finish', () => deferred.resolve()) 157 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 158 | fun(new Numbers()).sync().map(n => n + 1).map(n => n % 2).map(n => `${n}\n`).pipe(out) 159 | } 160 | }) 161 | 162 | suite.add('fun-async', { 163 | defer: true, 164 | fn (deferred) { 165 | const out = new NullSink() 166 | out.on('finish', () => deferred.resolve()) 167 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 168 | fun(new Numbers()).async().map(async n => n + 1).map(async n => n % 2).map(async n => `${n}\n`).pipe(out) 169 | } 170 | }) 171 | 172 | suite.add('fun-cb', { 173 | defer: true, 174 | fn (deferred) { 175 | const out = new NullSink() 176 | out.on('finish', () => deferred.resolve()) 177 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 178 | fun(new Numbers()).async().map((n, cb) => cb(null, n + 1)).map((n, cb) => cb(null, n % 2)).map((n, cb) => cb(null, `${n}\n`)).pipe(out) 179 | } 180 | }) 181 | 182 | suite.add('fun-promise', { 183 | defer: true, 184 | fn (deferred) { 185 | const out = new NullSink() 186 | out.on('finish', () => deferred.resolve()) 187 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 188 | fun(new Numbers()).async().map(n => Promise.resolve(n + 1)).map(n => Promise.resolve(n % 2)).map(n => Promise.resolve(`${n}\n`)).pipe(out) 189 | } 190 | }) 191 | 192 | suite.add('Array.map', { 193 | defer: true, 194 | fn (deferred) { 195 | const out = new NullSink() 196 | out.on('finish', () => deferred.resolve()) 197 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 198 | data.map(n => n + 1).map(n => n % 2).map(n => `${n}\n`).forEach(n => out.write(n)) 199 | out.end() 200 | } 201 | }) 202 | 203 | suite.add('Array.map-single', { 204 | defer: true, 205 | fn (deferred) { 206 | const out = new NullSink() 207 | out.on('finish', () => deferred.resolve()) 208 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 209 | data.map(n => `${(n + 1) % 2}\n`).forEach(n => out.write(n)) 210 | out.end() 211 | } 212 | }) 213 | 214 | suite.add('Array.map-single-oneloop', { 215 | defer: true, 216 | fn (deferred) { 217 | const out = new NullSink() 218 | out.on('finish', () => deferred.resolve()) 219 | data.forEach(n => out.write(`${(n + 1) % 2}\n`)) 220 | out.end() 221 | } 222 | }) 223 | 224 | suite.add('for-C (a)', { 225 | defer: true, 226 | fn (deferred) { 227 | const out = new NullSink() 228 | out.on('finish', () => deferred.resolve()) 229 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 230 | let ii 231 | for (ii = 0; ii < data.length; ++ii) { 232 | out.write(`${(data[ii] + 1) % 2}\n`) 233 | } 234 | out.end() 235 | } 236 | }) 237 | 238 | suite.add('for-C (b)', { 239 | defer: true, 240 | fn (deferred) { 241 | const out = new NullSink() 242 | out.on('finish', () => deferred.resolve()) 243 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 244 | for (let ii = 0; ii < data.length; ++ii) { 245 | out.write(`${(data[ii] + 1) % 2}\n`) 246 | } 247 | out.end() 248 | } 249 | }) 250 | 251 | suite.add('for-in', { 252 | defer: true, 253 | fn (deferred) { 254 | const out = new NullSink() 255 | out.on('finish', () => deferred.resolve()) 256 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 257 | for (let ii in data) { 258 | out.write(`${(data[ii] + 1) % 2}\n`) 259 | } 260 | out.end() 261 | } 262 | }) 263 | 264 | suite.add('for-of', { 265 | defer: true, 266 | fn (deferred) { 267 | const out = new NullSink() 268 | out.on('finish', () => deferred.resolve()) 269 | out.on('error', err => deferred.resolve(console.error('ERR', err))) 270 | for (var n of data) { 271 | out.write(`${(n + 1) % 2}\n`) 272 | } 273 | out.end() 274 | } 275 | }) 276 | } 277 | -------------------------------------------------------------------------------- /fun-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fun = require('./index.js') 3 | const mixinPromiseStream = require('./mixin-promise-stream.js') 4 | const is = require('isa-stream') 5 | let FilterStream 6 | let MapStream 7 | let MutateStream 8 | let FlatMapStream 9 | let ReduceStream 10 | let ForEachStream 11 | let LineStream 12 | 13 | const OPTS = Symbol('opts') 14 | const ISFUN = Symbol('isFun') 15 | const PROMISES = Symbol('promises') 16 | const RESULT = Symbol('result') 17 | const PIPE = Symbol('pipe') 18 | class FunStream { 19 | init (opts) { 20 | this[OPTS] = Object.assign({Promise: Promise}, opts || {}) 21 | this[ISFUN] = true 22 | this[PROMISES] = {} 23 | this[RESULT] = null 24 | this.fun = { ended: () => this.fun$ended(), finished: () => this.fun$finished(), writable: () => this.fun$writable() } 25 | } 26 | fun$ended () { 27 | if (!is.Readable(this)) throw new TypeError('This stream is not a readable stream, it will not end. Try `.finished()` instead.') 28 | if (this[PROMISES].ended) return this[PROMISES].ended 29 | return this[PROMISES].ended = new this[OPTS].Promise((resolve, reject) => { 30 | this.once('error', reject) 31 | this.once('end', () => setImmediate(resolve, this[RESULT])) 32 | }) 33 | } 34 | fun$finished () { 35 | if (!is.Writable(this)) throw new TypeError('This stream is not a writable stream, it will not finish. Try `.ended()` instead.') 36 | if (this[PROMISES].finished) return this[PROMISES].finished 37 | return this[PROMISES].finished = new this[OPTS].Promise((resolve, reject) => { 38 | this.once('error', reject) 39 | this.once('finish', () => setImmediate(resolve, this[RESULT])) 40 | }) 41 | } 42 | fun$closed () { 43 | if (!is.Writable(this)) throw new TypeError('This stream is not a writable stream, it will not close. Try `.ended()` instead.') 44 | if (this[PROMISES].closed) return this[PROMISES].closed 45 | return this[PROMISES].closed = new this[OPTS].Promise((resolve, reject) => { 46 | this.once('error', reject) 47 | this.once('close', resolve) 48 | }) 49 | } 50 | fun$writable () { 51 | if (!is.Writable(this)) throw new TypeError("This stream is not a writable stream, so it can't be... writable.") 52 | if (this.writable) return Promise.resolve() 53 | return new Promise(resolve => { 54 | this.once('drain', resolve) 55 | }) 56 | } 57 | async (todo) { 58 | if (todo) { 59 | const value = this[OPTS].async 60 | this[OPTS].async = true 61 | const next = todo.call(this, this) 62 | next[OPTS].async = value 63 | return next 64 | } else { 65 | this[OPTS].async = true 66 | return this 67 | } 68 | } 69 | sync (todo) { 70 | if (todo) { 71 | const value = this[OPTS].async 72 | this[OPTS].async = false 73 | const next = todo.call(this, this) 74 | next[OPTS].async = value 75 | return next 76 | } else { 77 | this[OPTS].async = false 78 | return this 79 | } 80 | } 81 | filter (filterWith, opts) { 82 | if (!FilterStream) FilterStream = require('./filter-stream.js') 83 | const filter = FilterStream(filterWith, opts ? Object.assign(this[OPTS], opts) : this[OPTS]) 84 | return this.pipe(filter) 85 | } 86 | map (mapWith, opts) { 87 | if (!MapStream) MapStream = require('./map-stream.js') 88 | const map = MapStream(mapWith, opts ? Object.assign(this[OPTS], opts) : this[OPTS]) 89 | return this.pipe(map) 90 | } 91 | mutate (mutateWith, opts) { 92 | if (!MutateStream) MutateStream = require('./mutate-stream.js') 93 | const mutate = MutateStream(mutateWith, opts ? Object.assign(this[OPTS], opts) : this[OPTS]) 94 | return this.pipe(mutate) 95 | } 96 | flat (opts) { 97 | return this.sync(o => o.flatMap(v => v, opts)) 98 | } 99 | flatMap (mapWith, opts) { 100 | if (!FlatMapStream) FlatMapStream = require('./flat-map-stream.js') 101 | const map = FlatMapStream(mapWith, opts ? Object.assign(this[OPTS], opts) : this[OPTS]) 102 | return this.pipe(map) 103 | } 104 | lines (opts) { 105 | if (!LineStream) LineStream = require('./line-stream.js') 106 | const lines = new LineStream(opts) 107 | return this.pipe(lines) 108 | } 109 | head (maxoutput) { 110 | let seen = 0 111 | return this.sync(o => o.filter(() => seen++ < maxoutput)) 112 | } 113 | reduce (reduceWith, initial, reduceOpts) { 114 | if (!ReduceStream) ReduceStream = require('./reduce-stream.js') 115 | const opts = Object.assign({}, this[OPTS], reduceOpts || {}) 116 | return this.pipe(ReduceStream(reduceWith, initial, opts)) 117 | } 118 | reduceTo (reduceWith, initial, reduceOpts) { 119 | const opts = Object.assign({}, this[OPTS], reduceOpts || {}) 120 | let reduceToWith 121 | if (isAsync(reduceWith, 2, opts)) { 122 | reduceToWith = (acc, value, cb) => { 123 | return new opts.Promise((resolve, reject) => { 124 | const result = reduceWith(acc, value, err => err ? reject(err) : resolve(acc)) 125 | if (result && result.then) result.then(() => resolve(acc), reject) 126 | }) 127 | } 128 | } else { 129 | /* eslint no-sequences:0 */ 130 | reduceToWith = (acc, value) => (reduceWith(acc, value), acc) 131 | } 132 | return this.reduce(reduceToWith, initial, opts) 133 | } 134 | reduceToObject (reduceWith, opts) { 135 | return this.reduceTo(reduceWith, {}, opts) 136 | } 137 | reduceToArray (reduceWith, opts) { 138 | return this.reduceTo(reduceWith, [], opts) 139 | } 140 | list (opts) { 141 | return this.sync(o => o.reduceToArray((acc, val) => acc.push(val), opts)) 142 | } 143 | grab (whenDone, opts) { 144 | return fun(this.list().then(v => whenDone(v))) 145 | } 146 | sort (sortWith, opts) { 147 | return this.grab(v => v.sort(sortWith)) 148 | } 149 | concat (opts) { 150 | return this.sync(o => o.reduce((acc, val) => acc + String(val), '', opts)) 151 | } 152 | ndjson (opts) { 153 | return this.fromNdjson(opts) 154 | } 155 | json (opts) { 156 | return this.fromJson(opts) 157 | } 158 | fromNdjson (opts) { 159 | return this.lines(opts).flatMap(_ => _ === '' ? [] : JSON.parse(_), opts) 160 | } 161 | fromJson (opts) { 162 | return this.concat().then(str => JSON.parse(str)) 163 | } 164 | toJson (opts) { 165 | return this.grab(_ => JSON.stringify(_), opts) 166 | } 167 | toNdjson (opts) { 168 | return this.map(_ => JSON.stringify(_) + '\n', opts) 169 | } 170 | forEach (forEachWith, forEachOpts) { 171 | if (!ForEachStream) ForEachStream = require('./for-each-stream.js') 172 | const opts = Object.assign({}, this[OPTS], forEachOpts || {}) 173 | return this.pipe(ForEachStream(forEachWith, opts)) 174 | } 175 | pipe (into, opts) { 176 | this.on('error', err => { 177 | if (err && err.src === undefined) err.src = this 178 | into.emit('error', err) 179 | }) 180 | const funified = fun(this[PIPE](into, opts), this[OPTS], opts && opts.what) 181 | return funified 182 | } 183 | } 184 | 185 | // collect (opts) is an alias of list 186 | FunStream.prototype.collect = FunStream.prototype.list 187 | FunStream.isFun = stream => Boolean(stream && stream[ISFUN]) 188 | FunStream.mixin = mixinFun 189 | FunStream.isAsync = isAsync 190 | FunStream.funInit = function () { 191 | const fn = this.init ? this.init 192 | : this.prototype && this.prototype.init ? this.prototype.init 193 | : FunStream.prototype.init 194 | return fn.apply(this, arguments) 195 | } 196 | 197 | FunStream.OPTS = OPTS 198 | 199 | function isAsync (fun, args, opts) { 200 | if (fun.constructor.name === 'AsyncFunction') return true 201 | if (opts && opts.async != null) return opts.async 202 | return fun.length > args 203 | } 204 | 205 | function mixinFun (stream, opts) { 206 | if (FunStream.isFun(stream)) return stream 207 | 208 | const cls = typeof stream === 'function' ? stream : null 209 | !cls && mixinPromiseStream(stream, Object.assign({Promise: fun.Promise}, opts || {})) 210 | const obj = cls ? cls.prototype : stream 211 | 212 | if (cls) { 213 | cls.isFun = FunStream.isFun 214 | cls.mixin = FunStream.mixin 215 | cls.isAsync = FunStream.isAsync 216 | cls.funInit = FunStream.funInit 217 | } else { 218 | FunStream.funInit.call(obj, opts) 219 | } 220 | 221 | if (is.Writable(obj)) { 222 | if (!cls || !obj.fun$writable) obj.fun$writable = FunStream.prototype.fun$writable 223 | if (!cls || !obj.fun$finished) obj.fun$finished = FunStream.prototype.fun$finished 224 | if (!cls || !obj.fun$closed) obj.fun$closed = FunStream.prototype.fun$closed 225 | } 226 | if (is.Readable(obj)) { 227 | if (!cls || !obj.fun$ended) obj.fun$ended = FunStream.prototype.fun$ended 228 | } 229 | if (!cls || !obj.filter) obj.filter = FunStream.prototype.filter 230 | if (!cls || !obj.map) obj.map = FunStream.prototype.map 231 | if (!cls || !obj.mutate) obj.mutate = FunStream.prototype.mutate 232 | if (!cls || !obj.flat) obj.flat = FunStream.prototype.flat 233 | if (!cls || !obj.flatMap) obj.flatMap = FunStream.prototype.flatMap 234 | if (!cls || !obj.head) obj.head = FunStream.prototype.head 235 | if (!cls || !obj.reduce) obj.reduce = FunStream.prototype.reduce 236 | if (!cls || !obj.reduceTo) obj.reduceTo = FunStream.prototype.reduceTo 237 | if (!cls || !obj.reduceToArray) obj.reduceToArray = FunStream.prototype.reduceToArray 238 | if (!cls || !obj.reduceToObject) obj.reduceToObject = FunStream.prototype.reduceToObject 239 | if (!cls || !obj.concat) obj.concat = FunStream.prototype.concat 240 | if (!cls || !obj.list) obj.list = FunStream.prototype.list 241 | if (!cls || !obj.lines) obj.lines = FunStream.prototype.lines 242 | if (!cls || !obj.json) obj.json = FunStream.prototype.json 243 | if (!cls || !obj.ndjson) obj.ndjson = FunStream.prototype.ndjson 244 | if (!cls || !obj.toJson) obj.toJson = FunStream.prototype.toJson 245 | if (!cls || !obj.toNdjson) obj.toNdjson = FunStream.prototype.toNdjson 246 | if (!cls || !obj.fromJson) obj.fromJson = FunStream.prototype.fromJson 247 | if (!cls || !obj.fromNdjson) obj.fromNdjson = FunStream.prototype.fromNdjson 248 | if (!cls || !obj.collect) obj.collect = FunStream.prototype.collect 249 | if (!cls || !obj.grab) obj.grab = FunStream.prototype.grab 250 | if (!cls || !obj.sort) obj.sort = FunStream.prototype.sort 251 | if (!cls || !obj.forEach) obj.forEach = FunStream.prototype.forEach 252 | if (!cls || !obj.sync) obj.sync = FunStream.prototype.sync 253 | if (!cls || !obj.async) obj.async = FunStream.prototype.async 254 | if (!cls || !obj.whenWritable) obj.whenWritable = FunStream.prototype.whenWritable 255 | 256 | obj[PIPE] = obj.pipe 257 | Object.defineProperty(obj, 'pipe', { 258 | value: FunStream.prototype.pipe, 259 | writable: true 260 | }) 261 | return obj 262 | } 263 | module.exports = FunStream 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # funstream 2 | 3 | Funstream gives you iteratorish methods on your streams. 4 | 5 | ```js 6 | const fun = require('funstream') 7 | 8 | /***** USAGE EXAMPLES *****/ 9 | 10 | // Make a stream, fun, then run some list comprehensions on it 11 | fun(boringStream) 12 | .map(n => n + 1) 13 | .filter(n => n % 2) 14 | .map(n => `${n}\n`) 15 | .pipe(process.stdout) 16 | // it's still a stream all the way through so each chunk is processed as its read 17 | 18 | // funstreams are also promises, which is particularly useful when 19 | // collection results from something like reduce 20 | fun(boringStream) 21 | .map(n => n + 1) 22 | .filter(n => n % 2) 23 | .reduce((a, b) => a + b) 24 | .then(console.log) 25 | 26 | /***** CONSTRUCTING *****/ 27 | 28 | // We can make just about anything a funstream 29 | // readable streams… 30 | fun(process.stdin) 31 | 32 | // Promises of streams... which means you can immediately attach your body stream processing and it'll 33 | // start getting data once the fetch completes. 34 | fun(fetch('https://example.com').then(r => r.body)) 35 | 36 | // generators… 37 | function * mygen () { 38 | for (let ii = 0; ii < 10000; ++ii) { 39 | yield ii 40 | } 41 | } 42 | fun(mygen()) 43 | 44 | // async generators 45 | async function * myasyncgen () { 46 | for (let ii = 0; ii < 10000; ++ii) { 47 | yield ii 48 | } 49 | } 50 | fun(myasyncgen()) 51 | 52 | // and on node 10, you can iterate a funstream 53 | for await (let value of fun(…)) { 54 | … 55 | } 56 | 57 | // arrays 58 | fun([1, 2, 3, 4]) 59 | 60 | // even writable streams, which can be treated as promises to see when they resolve or reject 61 | fun(writestream) 62 | .then(() => console.log('finished!')) 63 | .catch(err => console.error('stream error', err) 64 | 65 | // fun writable streams are streams, continue to be writable streams even 66 | // though you can use them likes promises 67 | process.stdin.pipe(fun(writestream)) 68 | .then(() => console.log('done!')) 69 | 70 | // fun streams can also be piped into, handy when the source stream isn't a 71 | // standard Node.js stream, but does implement the pipe interface. 72 | process.stdin.pipe(fun()) 73 | .map(str => transformStr(str)) 74 | .pipe(process.stdout) 75 | 76 | // You can bundle up a series of transforms into single ttransform stream 77 | const mytransformStream = 78 | fun(stream => stream.map(str => str.toUpperCase). 79 | flatMap(v = [v, v])) 80 | 81 | /***** ADDED METHODS *****/ 82 | 83 | // Fun functions can be sync… 84 | .map(str => str.slice(10)) 85 | 86 | // Fun functions can be async… 87 | .map(async str => (await transformStr(str)).slice(10)) 88 | 89 | // Fun functions can be promise returning… 90 | .map(str => transformStr(str), {async: true}) 91 | 92 | // Fun functions can be callback using… 93 | .map((str, cb) => transformStrCB(str, cb), {async: true}) 94 | ``` 95 | 96 | Funstream makes object streams better. 97 | 98 | ## Funstream constructors 99 | 100 | ### fun(readableStream[, opts]) → FunStream 101 | 102 | This is probably what you want. 103 | 104 | Makes an existing stream a funstream! Has the advantage over `fun()` of 105 | handling error propagation for you. All funs are promises of their 106 | completion too, so you can `await` or `.catch` your stream. 107 | 108 | `opts` is an optional options object. The only option currently is `async` 109 | which let's you explicitly tell Funstream if your callbacks are sync or 110 | async. If you don't include this we'll detect which you're using by looking 111 | at the number of arguments your callback takes. Because promises and sync functions 112 | take the same number of arguments, if you're using promise returning callbacks you'll need to 113 | explicitly pass in `async: true`. 114 | 115 | ### fun(callback[, opts]) → FunStream 116 | 117 | This lets you bundle a fun-stream pipeline up into a single transform stream 118 | that you might pass to something else. The callback receives a FunStream as 119 | its only argument, chain off of that as you like and return the result. The 120 | stream returned by `fun()` will write to that first FunStream and read from 121 | the end of your chain. (With the usual error propagation.) 122 | 123 | ### fun(writableStream[, opts]) → PromiseStream 124 | 125 | Writable streams can't be fun per se, since being fun means having 126 | iterators. What we can do is make them resolvable as promises, with an extra 127 | feature or two. 128 | 129 | ### fun(array[,opts]) → FunStream 130 | 131 | Returns a funstream that will receive entries from the array one at a time 132 | while respecting back pressure. 133 | 134 | ### fun(string[,opts]) → FunStream 135 | 136 | Returns a funstream that will receive entries from the array one at a time 137 | while respecting back pressure. 138 | 139 | ### fun(iterator[,opts]) → FunStream 140 | 141 | Returns a funstream that will receive values from the iterator one at a time 142 | while respecting back pressure. 143 | 144 | ### fun(asyncIterator[,opts]) → FunStream 145 | 146 | Returns a funstream that will receive values from the async iterator one at a time 147 | while respecting back pressure. 148 | 149 | ### fun(promise[,opts]) → FunStream 150 | 151 | Returns a funstream that will consume the result of the promise exactly as 152 | the equivalent plain value would be. No data will be emitted until the promise 153 | resolves. If it rejects it will be propagated as an error in the usual ways. 154 | 155 | These are special, because the returned object only lazily becomes a stream. If you 156 | treat it as a promise then no stream construction occurs. 157 | 158 | ### fun([opts]) → FunStream 159 | 160 | Make a passthrough Funstream. You can pipe into this to get access to our 161 | handy methods. 162 | 163 | ### fun.with(todo[, opts]) → FunStream 164 | 165 | For those times when you want to create a stream from nothing, or at least, 166 | from a non-stream source, `fun.with` provides any easy interface for doing 167 | that. 168 | 169 | Pass it a function and you'll get a stream you can write to. You can close 170 | it off by just resolving your promise which is particularly convenient when 171 | your function is declared `async`. 172 | 173 | Constructs a passthrough funstream and passes it as an argument to `todo`. 174 | `todo` is a function that returns a `Promise`. When the `Promise` resolves the 175 | stream will end. 176 | 177 | ```js 178 | const sleep = (ms, ...args) => new Promise(_ => setTimeout(_, ms, ...args)) 179 | const result = await fun.with(async st => { 180 | for (let count = 0; count < 5; ++count) { 181 | st.write(count) 182 | await sleep(100) 183 | } 184 | }).list() // [ 0, 1, 2, 3, 4 ] 185 | ``` 186 | ```js 187 | // demonstrating identity here, you wouldn't want to do this: 188 | let a 189 | let b = fun.with(async st => { a = st }) 190 | a === b // true 191 | ``` 192 | 193 | ### fun.FunStream 194 | 195 | Exactly the same as `stream.PassThrough` but with fun added. `fun()` is 196 | mostly the same as `new fun.FunStream()`. (The former will use Bluebird for 197 | promises if available but fallback to system promises. The latter has no 198 | magic and just uses system promises.) 199 | 200 | ### require('funstream/fun-stream').mixin 201 | 202 | The core extension mechanism (otherwise unneeded). It adds fun to an 203 | existing class or object. Classes that have fun mixed in need to also call 204 | `FunPassThrough.funInit.call(this, opts)` in their constructors. 205 | 206 | ## Funstream and Pipelines 207 | 208 | Contrary to ordinary, BORING streams, we make sure errors are passed along 209 | when we chain into something. This applies when you `.map` or `.filter` but 210 | it ALSO applies when you `.pipe`. 211 | 212 | ## PromiseStream methods 213 | 214 | ### .fun.finished() → Promise 215 | 216 | Available on Writable promise streams, the returned Promise will resolved 217 | when the stream emits a `finish` event. The promise will be rejected if the 218 | stream emits an `error` event. 219 | 220 | If the stream emits a `result` event then the stream will resolve with that 221 | value. 222 | 223 | ### .fun.closed() → Promise 224 | 225 | Available on Writable promise streams, the returned Promise will resolved 226 | when the stream emits a `close` event. The promise will be rejected if the 227 | stream emits an `error` event. 228 | 229 | NOTE: Not all streams emit a `close` event and if you use this on a stream 230 | that does not then it will never resolve. 231 | 232 | ### .async() → this 233 | ### .sync() → this 234 | 235 | Sets the stream `async` stream option to true and false respectively. 236 | 237 | ### .async(todo) → FunStream 238 | ### .sync(todo) → FunStream 239 | 240 | Runs `todo` with a stream with the appropriate `async` option set. The 241 | returned value is restored to the previous setting. 242 | 243 | ``` 244 | fun([1,2,3]) 245 | .filter(async n => n > 0) 246 | .sync(str => str.filter(n => n < 3).map(n => n * 2)) 247 | ``` 248 | 249 | ### .fun.ended() → Promise 250 | 251 | Returns a Promise that resolves when the stream emits an `end` event. If 252 | the stream emits an `error` event then it will reject. 253 | 254 | ### .fun.writable() → Promise(Boolean) 255 | 256 | _With Node >= 11.4.0:_ 257 | 258 | Returns a promise that resolves when the stream is writable to ensure you 259 | don't bloat out the buffers of a stream that is slow to consume data: 260 | 261 | ``` 262 | await stream.fun.writable() 263 | stream.write('my chunk of data') 264 | ``` 265 | 266 | Under the hood what this does is check the `writable` flag introduced in 267 | 11.4, if that's true then it just resolves, if false it attaches itself to 268 | the drain event and resolves when that happens. 269 | 270 | _With Node < 11.4.0:_ 271 | 272 | Prior to 11.4, you have to track writable status yourself, but you can still 273 | use `fun.writable()` to be notified when the stream is ready for more data. 274 | 275 | DANGER: Be sure that the stream is NOT writable before calling 276 | `fun.writable` or the promise may never resolve (because the underlying 277 | stream won't emit a `drain` event.) 278 | 279 | ``` 280 | const writable = stream.write('my chunk of data') 281 | if (!writable) await stream.fun.writable() 282 | ``` 283 | 284 | ## FunStream methods 285 | 286 | This is the good stuff. All callbacks can be sync or async. You can 287 | indicate this by setting the `async` property on the opts object either when 288 | calling the method below or when constructing the objects to start with. 289 | Values of the `async` property propagate down the chain, for example: 290 | 291 | `.map(…, {async: true}).map(…)` 292 | 293 | The second map callback will also be assume do to be async. 294 | 295 | Multiple sync functions of the same time will be automatically aggregated 296 | without constructing additional streams, so: 297 | 298 | `.filter(n => n < 23).filter(n => n > 5)` 299 | 300 | The second `filter` call actually returns the same stream object. This does 301 | mean that if you try to fork the streams in between it won't work. Sorry. 302 | 303 | ### .pipe(target[, opts]) → FunStream(target) 304 | 305 | Like an ordinary pipe, but funerer. In addition mutating the target into a 306 | funstream we also forward errors to it. 307 | 308 | ### .head(numberOfItems) → FunStream 309 | 310 | Will only forward the first `numberOfItems` down stream. The remainder are 311 | ignored. At the moment this does not end the stream after the 312 | `numberOfItems` limit is hit, but in future it likely will. 313 | 314 | ```js 315 | fun(stream) 316 | .head(5) 317 | .forEach(item => { // only sees the first five items regardless of how long the stream is. 318 | }) 319 | ``` 320 | 321 | ### .filter(filterWith[, opts]) → FunStream 322 | 323 | Filter the stream! 324 | 325 | * `filterWith(data) → Boolean` (can throw) 326 | * `filterWith(data, cb)` (and `cb(err, shouldInclude)`) 327 | * `filterWith(data) → Promise(Boolean) 328 | 329 | If `filterWith` returns true, we include the value in the output stream, 330 | otherwise not. 331 | 332 | ### .map(mapWith[, opts]) → FunStream 333 | 334 | Transform the stream! 335 | 336 | * `mapWith(data) → newData` (can throw) 337 | * `mapWith(data, cb)` (and `cb(err, newData)`) 338 | * `mapWith(data) → Promise(newData) 339 | 340 | `data` is replaced with `newData` from `mapWith` in the output stream. 341 | 342 | ### .mutate(mutateWith[, opts]) → FunStream 343 | 344 | `stream.mutate(data => {…})` is sugar for `stream.map(data => {…; return data})` 345 | 346 | ### .flat([opts]) → FunStream 347 | 348 | Flattens arrays in the streams into object emissions! That is to say, a stream of two objects: 349 | 350 | ```js 351 | [1, 2, 3], [23, 42, 57] 352 | ``` 353 | 354 | Will become a stream of six objects: 355 | 356 | ```js 357 | 1, 2, 3, 23, 42, 57 358 | ``` 359 | 360 | This is implemented as `stream.flatMap(v => v, opts)` 361 | 362 | ### .flatMap([opts]) → FunStream 363 | 364 | Transform all the stream elements and flatten any return values. This is 365 | the equivalent of: 366 | 367 | ```js 368 | stream.map(…).flat() 369 | ``` 370 | 371 | Only without multiple phases. 372 | 373 | ### .lines([opts]) → FunStream 374 | 375 | Parse the input stream into lines, emitting one line per chunk. Newlines are removed. 376 | 377 | ### .ndjson([opts]) → FunStream 378 | ### .fromNdjson([opts]) → FunStream 379 | 380 | Parse the input stream as newline delimited JSON, emitting one parsed JSON 381 | object per line. Empty lines are ignored. 382 | 383 | ### .toNdjson([opts]) → FunStream 384 | 385 | Take an input object stream and emit as newline delimited JSON. Sugar for: 386 | 387 | ```js 388 | stream.map(_ => JSON.stringify(_) + '\n', opts) 389 | ``` 390 | 391 | ### .sort(sortWith[, opts]) → FunStream 392 | 393 | WARNING: This has to load all of your content into memory in order to sort 394 | it, so be sure to do your filtering or limiting (with `.head`) before you 395 | call this. This results in a funstream fed from the sorted array. 396 | 397 | `sortWith(a, b) → -1 | 0 | 1` – It's the usual sort comparison function. It 398 | must be synchronous as it's ultimately passed to `Array.sort`. 399 | 400 | Sort a stream alphabetically: 401 | 402 | ```js 403 | fun(stream) 404 | .sort((a, b) => a.localeCompare(b)) 405 | ``` 406 | 407 | ### .grab(grabWith[, opts]) → FunStream 408 | 409 | WARNING: This has to load all of your content into memory in order to sort 410 | it, so be sure to do your filtering or limiting (with `.head`) before you 411 | call this. This results in a funstream fed from the sorted array. 412 | 413 | `grabWith` is a synchronous function. It takes an array as an argument and 414 | turns the return value back into a stream with `fun()`. The array 415 | is produced by reading the entire stream, so be warned. 416 | 417 | For example, sort can be implemented as: 418 | 419 | ```js 420 | function sortStream (st) { 421 | return st.grab(v => v.sort(sortWith)) 422 | } 423 | ``` 424 | 425 | It makes it easy to apply array verbs to a stream that aren't otherwise 426 | supported but it does mean loading the entire stream into memory. 427 | 428 | It's the equivalent of `fun(grabWith(await stream.list()))` 429 | 430 | ### .list([opts]) → FunStream 431 | 432 | Alias: `.collect(opts)` 433 | 434 | Promise an array of all of the values in the stream. Let's you do things like… 435 | 436 | ```js 437 | const data = await fun().map(…).filter(…).list() 438 | ``` 439 | 440 | 441 | It's just sugar for: `reduceToArray((acc, val) => acc.push(val), opts)` 442 | 443 | ### .concat([opts]) → FunStream 444 | 445 | Promise a string produced by concatenating all of the values in the stream. 446 | 447 | ### .json([opts]) → PromiseStream 448 | ### .fromJson([opts]) → PromiseStream 449 | 450 | Promise an object produced by JSON parsing the result of `.concat()`. Sugar for: 451 | 452 | ```js 453 | stream.concat().then(str => JSON.parse(str)) 454 | ``` 455 | 456 | ### .toJson([opts]) -> PromiseStream 457 | 458 | Given a stream of objects, produces a JSON stringified array of them. Sugar for: 459 | 460 | ```js 461 | stream.grab(_ => JSON.stringify(_), opts) 462 | ``` 463 | 464 | ### .reduce(reduceWith[, initial[, opts]]) → FunStream 465 | 466 | Promise the result of computing everything. 467 | 468 | * `reduceWith(acc, value) → acc` (can throw) 469 | * `reduceWith(acc, value, cb)` (and `cb(err, acc)`) 470 | * `reduceWith(acc, value) → Promise(acc) 471 | 472 | Concat a stream: 473 | ```js 474 | fun(stream) 475 | .reduce((acc, value) => acc + value) 476 | .then(wholeThing => { … }) 477 | ``` 478 | 479 | The return value is _also_ a stream, so you can hang the usual event 480 | listeners off it. Reduce streams emit a `result` event just before 481 | `finish` with the final value of the accumulator in the reduce. 482 | 483 | ### .reduceToArray(reduceWith[, opts]) → FunStream 484 | 485 | Promise the result of reducing into an array. Handy when you want to push 486 | on to an array without worrying about your return value. This is sugar for: 487 | 488 | ```js 489 | fun(stream) 490 | .reduce((acc, value) => { reduceWith(acc, value) ; return acc }, []) 491 | ``` 492 | 493 | ### .reduceToArray(reduceWith[, opts]) → FunStream 494 | 495 | Promise the result of reducing into an array. Handy when you want to build 496 | an object without worrying about your return values. This is sugar for: 497 | 498 | ```js 499 | fun(stream) 500 | .reduce((acc, value) => { reduceWith(acc, value) ; return acc }, {}) 501 | ``` 502 | 503 | ### .forEach(consumeWith[, opts]) → PromiseStream 504 | 505 | Run some code for every chunk, promise that the stream is done. 506 | 507 | Example, print each line: 508 | ```js 509 | fun(stream) 510 | .forEach(chunk => console.log(chunk) 511 | .then(() => console.log('Done!')) 512 | ``` 513 | 514 | As with reduce streams the return value from `forEach` is both a promise 515 | and a stream. 516 | 517 | ## Benchmarks 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 |
map: fun sync565 ops/s
map: fun async (cb)454 ops/s
map: stream.Transform403 ops/s
map: through2311 ops/s
map: fun async (async/await)304 ops/s
map: fun async (new Promise)237
527 | --------------------------------------------------------------------------------