pipe-iterators
Version:
Like underscore for Node streams. Map, reduce, filter, fork, pipeline and other utility functions for iterating over object mode streams.
474 lines (409 loc) • 12.2 kB
JavaScript
var assert = require('assert'),
pi = require('../index.js'),
isReadable = require('../lib/is-stream').isReadable,
isWritable = require('../lib/is-stream').isWritable,
isDuplex = require('../lib/is-stream').isDuplex,
child_process = require('child_process');
describe('match', function() {
it('returns a writable stream, accepts multiple condition + stream pairs', function(done) {
var twos = [],
threes = [],
result = pi.match(
function(obj) { return obj % 2 == 0; },
pi.toArray(twos),
function(obj) { return obj % 3 == 0; },
pi.toArray(threes),
pi.devnull()
);
assert.ok(isWritable(result));
assert.ok(!isReadable(result));
pi.fromArray([ 1, 2, 3, 4, 5, 6 ])
.pipe(result)
.once('finish', function() {
assert.deepEqual(twos, [ 2, 4, 6]);
assert.deepEqual(threes, [ 3 ]); // matched in order
done();
});
});
it('works when given a single condition and stream', function(done) {
var twos = [],
result = pi.match(
function(obj) { return obj % 2 == 0; },
pi.toArray(twos),
pi.devnull()
);
assert.ok(isWritable(result));
assert.ok(!isReadable(result));
pi.fromArray([ 1, 2, 3, 4, 5, 6 ])
.pipe(result)
.once('finish', function() {
assert.deepEqual(twos, [ 2, 4, 6]);
done();
});
});
it('works when given a single stream (rest)', function(done) {
var all = [],
result = pi.match(
pi.toArray(all)
);
assert.ok(isWritable(result));
assert.ok(!isReadable(result));
pi.fromArray([ 1, 2, 3, 4, 5, 6 ])
.pipe(result)
.once('finish', function() {
assert.deepEqual(all, [ 1, 2, 3, 4, 5, 6 ]);
done();
});
});
it('accepts a last parameter which is a stream for non-matching elements', function(done){
var twos = [],
threes = [],
rest = [],
result = pi.match(
function(obj) { return obj % 2 == 0; },
pi.toArray(twos),
function(obj) { return obj % 3 == 0; },
pi.toArray(threes),
pi.toArray(rest)
);
assert.ok(isWritable(result));
assert.ok(!isReadable(result));
pi.fromArray([ 1, 2, 3, 4, 5, 6 ])
.pipe(result)
.once('finish', function() {
assert.deepEqual(twos, [ 2, 4, 6]);
assert.deepEqual(threes, [ 3 ]); // matched in order
assert.deepEqual(rest, [ 1, 5]);
done();
});
});
function errorStream() {
return pi.thru.obj(function(chunk, enc, done) {
this.emit('error', new Error('Expected error'));
this.push(chunk);
done();
});
}
function always() { return true; }
function never() { return false; }
it('listening on error captures errors emitted in the first stream', function(done) {
var stream = pi.match(always, errorStream(), never, pi.thru.obj(), pi.thru.obj());
stream.once('error', function(err) {
assert.ok(err);
done();
});
pi.fromArray(1).pipe(stream);
});
it('listening on error captures errors emitted in the second stream', function(done) {
var stream = pi.match(never, pi.thru.obj(), always, errorStream(), pi.thru.obj());
stream.once('error', function(err) {
assert.ok(err);
done();
});
pi.fromArray(1).pipe(stream);
});
it('listening on error captures errors emitted in the rest stream', function(done) {
var stream = pi.match(never, pi.thru.obj(), never, pi.thru.obj(), errorStream());
stream.once('error', function(err) {
assert.ok(err);
done();
});
pi.fromArray(1).pipe(stream);
});
});
describe('fork', function() {
it('returns a duplex stream', function() {
var result = pi.fork();
assert.ok(isWritable(result));
assert.ok(isReadable(result));
});
it('prevents streams from interfering with each other by cloning', function(done) {
var inputs = [ { id: 1 }, { id: 2 } ],
result1 = [],
result2 = [];
pi.fromArray(inputs)
.pipe(pi.fork(
pi.head(pi.mapKey('foo', function() { return 'bar'; }), pi.toArray(result1)),
pi.head(pi.mapKey('id', function(val) { return val * 2; }), pi.toArray(result2))
)).once('finish', function() {
assert.deepEqual(result1, [ { id: 1, foo: 'bar'}, { id: 2, foo: 'bar' }]);
assert.deepEqual(result2, [ { id: 2 }, { id: 4 }]);
done();
});
});
});
describe('merge', function() {
it('returns a duplex stream', function() {
assert.ok(isDuplex(pi.merge()));
});
it('merges multiple streams', function(done) {
pi.merge(pi.fromArray(1, 2), pi.fromArray(3, 4), pi.fromArray(5, 6))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), [ 1, 2, 3, 4, 5, 6 ]);
done();
}));
});
it('merges multiple streams, arg is array', function(done) {
pi.merge([pi.fromArray(1, 2), pi.fromArray(3, 4), pi.fromArray(5, 6)])
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), [ 1, 2, 3, 4, 5, 6 ]);
done();
}));
});
it('works with just one stream', function(done) {
pi.merge(pi.fromArray(1))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), [ 1 ]);
done();
}));
});
it('works with one empty stream', function(done) {
pi.merge(pi.fromArray(1), pi.fromArray(), pi.fromArray(2))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), [ 1, 2 ]);
done();
}));
});
it('works with just empty streams', function(done) {
pi.merge(pi.fromArray(), pi.fromArray())
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), []);
done();
}));
});
it('works in flowing mode', function(done) {
var result = [];
pi.merge(pi.fromArray(1, 2), pi.fromArray(3, 4), pi.fromArray(5, 6))
.on('data', function(data) { result.push(data); })
.once('end', function() {
assert.deepEqual(result.sort(), [ 1, 2, 3, 4, 5, 6]);
done();
});
});
});
function logEvts(id, stream) {
// readable (non-flowing) stream
return stream.on('readable', function() {
console.log('[' + id +'] "readable"');
})
.on('end', function() {
console.log('[' + id +'] "end"');
})
.on('close', function() {
console.log('[' + id +'] "close"');
})
.on('error', function(err) {
console.log('[' + id +'] "error"', err);
})
// writable (non-flowing) stream
.on('drain', function() {
console.log('[' + id +'] "drain"');
})
.on('finish', function() {
console.log('[' + id +'] "finish"');
})
.on('pipe', function() {
console.log('[' + id +'] "pipe"');
})
.on('unpipe', function() {
console.log('[' + id +'] "unpipe"');
});
}
function logStream(id) {
return logEvts(id, pi.thru.obj(function(data, enc, done) {
console.log('[' + id + '] _transform ' + data);
this.push(data);
done();
}, function(done) {
console.log('[' + id +'] _flush');
done();
}));
}
describe('forkMerge', function() {
function doubler(val) { return val * 2; }
function add100(val) { return val + 100; }
it('combines a fork stream and a merge stream', function(done) {
pi.fromArray(1, 2, 3)
.pipe(
pi.forkMerge(
pi.pipeline(pi.map(doubler), pi.map(doubler)),
pi.pipeline(pi.map(add100), pi.map(add100))
)
).pipe(pi.toArray(function(result) {
assert.deepEqual(
result.sort(function(a, b){ return a-b; }),
[ 4, 8, 12, 201, 202, 203 ]
);
done();
}))
});
});
describe('matchMerge', function() {
function add10(val) { return val + 10; }
function add100(val) { return val + 100; }
it('combines a match stream and a merge stream', function(done) {
pi.fromArray([ 1, 2, 3, 4, 5, 6 ])
.pipe(pi.matchMerge(
function(obj) { return obj % 2 == 0; },
pi.map(add10),
function(obj) { return obj % 3 == 0; },
pi.map(add100),
pi.thru.obj()
))
.pipe(pi.toArray(function(result) {
assert.deepEqual(
result.sort(function(a, b){ return a-b; }),
[
1, // 1 -> 1
5, // 5 -> 5
12, // 2 -> + 10 -> 12
14, // 4 -> + 10 -> 14
16, // 6 -> + 10 -> 16
103 // 3 -> + 100 -> 103
]
);
done();
}));
});
});
describe('parallel', function() {
it('can execute a series of tasks in serial order', function(done) {
var calls = [];
pi.fromArray(1, 2, 3)
.pipe(pi.map(function(val, i) {
calls.push(i);
return function (done) {
this.push(val * 2);
done();
};
}))
.pipe(pi.parallel(1))
.pipe(pi.toArray(function(result) {
assert.deepEqual(calls, [ 0, 1, 2]);
assert.deepEqual(result, [ 2, 4, 6 ]);
done();
}));
});
it('can run the example', function(done) {
pi.fromArray([
function(done) { this.push(1); done(); },
function(done) { this.push(2); done(); }
])
.pipe(pi.parallel(2))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result.sort(), [1, 2]);
done();
}));
});
it('can execute a series of tasks with parallelism 2', function(done) {
pi.fromArray([
function(done) {
var self = this;
setTimeout(function() {
self.push(1);
done();
}, 50);
},
function(done) {
var self = this;
setTimeout(function() {
self.push(2);
done();
}, 100);
},
function(done) {
var self = this;
setTimeout(function() {
self.push(3);
done();
}, 25);
}
])
.pipe(pi.parallel(2))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result, [ 1, 3, 2 ]); // due to timeouts
done();
}));
});
it('can execute a series of tasks with infinite parallelism', function(done) {
pi.fromArray([
function(done) {
var self = this;
setTimeout(function() {
self.push(1);
done();
}, 50);
},
function(done) {
var self = this;
setTimeout(function() {
self.push(2);
done();
}, 100);
},
function(done) {
var self = this;
setTimeout(function() {
self.push(3);
done();
}, 25);
}
])
.pipe(pi.parallel(Infinity))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result, [ 3, 1, 2 ]); // due to timeouts
done();
}));
});
it('works with empty', function(done) {
pi.fromArray([])
.pipe(pi.parallel(Infinity))
.pipe(pi.toArray(function(result) {
assert.deepEqual(result, [ ]);
done();
}));
});
it('add to parallel while executing', function(done) {
var callOrder = [];
// there are no guarantees that one "done" action runs
// before another (unless you do parallelism = 1)
function checkDone() {
if (callOrder.length < 4) {
return;
}
var expected = [ '1-1', '1-2', '2-2', '2-1' ];
assert.ok(
expected.every(function(item) { return callOrder.indexOf(item) > -1; }),
'every callback should have run');
done();
}
pi.fromArray([
function a(done) {
callOrder.push('1-1');
done();
// add more tasks
this.write(function c(done) {
setTimeout(function() {
callOrder.push('2-1');
done();
checkDone();
}, 20);
});
this.write(function d(done) {
callOrder.push('2-2');
done();
checkDone();
});
},
function b(done) {
setTimeout(function() {
callOrder.push('1-2');
done();
checkDone();
}, 100);
}
])
.pipe(pi.parallel(1))
.pipe(pi.devnull());
});
});