pipe-iterators
Version:
Like underscore for Node streams. Map, reduce, filter, fork, pipeline and other utility functions for iterating over object mode streams.
407 lines (346 loc) • 11.3 kB
JavaScript
var through = require('through2'),
cloneLib = require('clone'),
Readable = require('readable-stream').Readable,
xtend = require('xtend');
var isStream = require('./lib/is-stream.js'),
Match = require('./lib/match.js');
// Iteration functions
exports.forEach = function(fn, thisArg) {
var index = 0;
thisArg = (typeof thisArg !== 'undefined' ? thisArg : null);
return through.obj(function(obj, enc, onDone) {
fn.call(thisArg, obj, index++);
this.push(obj);
onDone();
});
};
exports.map = function(fn, thisArg) {
var index = 0;
thisArg = (typeof thisArg !== 'undefined' ? thisArg : null);
return through.obj(function(obj, enc, onDone) {
this.push(fn.call(thisArg, obj, index++));
onDone();
});
};
exports.reduce = function(fn, initial) {
var index = 0,
captureFirst = (arguments.length < 2),
acc = (!captureFirst ? initial : null);
return through.obj(function(obj, enc, onDone) {
if (captureFirst) {
acc = obj;
captureFirst = false;
index++;
} else {
acc = fn(acc, obj, index++);
}
onDone();
}, function(onDone) {
this.push(acc);
onDone();
});
};
exports.filter = function(fn, thisArg) {
var index = 0;
thisArg = (typeof thisArg !== 'undefined' ? thisArg : null);
return through.obj(function(obj, enc, onDone) {
if (fn.call(thisArg, obj, index++)) { this.push(obj); }
onDone();
});
};
exports.mapKey = function(first, fn, thisArg) {
var index = 0;
if (typeof first === 'string' && typeof fn === 'function') {
thisArg = (typeof thisArg !== 'undefined' ? thisArg : null);
return through.obj(function(obj, enc, onDone) {
obj[first] = fn.call(thisArg, obj[first], obj, index++);
this.push(obj);
onDone();
});
} else if (typeof first === 'object' && first !== null) {
thisArg = (typeof fn !== 'undefined' ? fn : null);
return through.obj(function(obj, enc, onDone) {
Object.keys(first).forEach(function(key) {
fn = first[key];
if (typeof fn === 'function') {
obj[key] = fn.call(thisArg, obj[key], obj, index++);
} else {
obj[key] = fn;
}
});
this.push(obj);
onDone();
});
} else {
throw new Error('mapKey must be called with: (key, fn) or (hash).');
}
};
// Input and output
exports.fromArray = function(arr) {
var eof = false;
arr = (Array.isArray(arr) ? arr : Array.prototype.slice.call(arguments));
var stream = exports.readable.obj(function() {
var item;
if (arr.length > 0) {
do {
item = arr.shift();
} while(typeof item !== 'undefined' && this.push(item))
}
if (arr.length === 0 && !eof) {
// pushing null signals EOF
eof = true;
this.push(null);
}
});
return stream;
};
exports.toArray = function(fn) {
var endFn = typeof fn === 'function' ? fn : null,
arr = (Array.isArray(fn) ? fn : []),
stream = exports.writable.obj(function(chunk, enc, done) {
arr.push(chunk);
done();
});
if (endFn) {
stream.once('finish', function() {
endFn(arr);
arr = [];
});
}
return stream;
};
exports.fromAsync = function(callable) {
var called = false;
var returned = false;
var eof = false;
var arr;
var stream;
function read() {
var item;
if (!called) {
callable(function(err, results) {
returned = true;
if (err) {
stream.emit('error', err);
eof = true;
stream.push(null);
return;
}
arr = Array.isArray(results) ? results : [results];
read();
});
called = true;
return;
}
if (!returned) {
return;
}
if (arr.length > 0) {
do {
item = arr.shift();
} while(typeof item !== 'undefined' && stream.push(item))
}
if (arr.length === 0 && !eof) {
// pushing null signals EOF
eof = true;
stream.push(null);
}
}
stream = exports.readable.obj(read);
return stream;
}
// Constructing streams
exports.thru = exports.through = through;
exports.writable = require('./lib/writable.js');
exports.readable = require('./lib/readable.js');
exports.duplex = require('./lib/duplex.js');
// based on https://github.com/deoxxa/duplexer2/pull/6 (with an additional bugfix)
exports.combine = function(writable, readable) {
if (!isStream.isWritable(writable)) {
throw new Error('The first stream must be writable.');
}
if (!isStream.isReadable(readable)) {
throw new Error('The last stream must be readable.');
}
if (writable === readable) {
throw new Error('The two streams must not be === to each other.');
// ... because it would lead to a bunch of special cases related to duplicate calls
}
// convert node 0.8 readable to 0.10 readable stream
if (typeof readable.read !== 'function') {
readable = new Readable().wrap(readable);
}
var stream = exports.duplex.obj(function(chunk, enc, done) {
if (!writable.writable) {
return done(); // if the stream has already ended, stop writing to it
}
// Node 0.8.x writable streams do not accept the third parameter, done
var ok = writable.write(chunk, enc);
if (ok) {
done();
} else {
writable.once('drain', done);
}
}, forwardRead);
writable.once('finish', function() { stream.end(); });
stream.once('finish', function() { writable.end(); });
readable.once('end', function() { stream.push(null); });
writable.on('error', function(err) { stream.emit('error', err); });
readable.on('error', function(err) { stream.emit('error', err); });
function forwardRead() {
var data, waitingToRead = true;
while ((data = readable.read()) !== null) {
waitingToRead = false;
stream.push(data);
}
if (waitingToRead) {
readable.once('readable', forwardRead);
}
}
return stream;
};
exports.cap = function(duplex) {
var stream = exports.writable.obj(function(chunk, enc, done) {
// Node 0.8.x writable streams do not accept the third parameter, done
var ok = duplex.write(chunk, enc);
if (ok) {
done();
} else {
duplex.once('drain', done);
}
});
duplex.once('finish', function() { stream.end(); });
stream.once('finish', function() { duplex.end(); });
duplex.on('error', function(err) { return stream.emit('error', err); });
return stream;
};
exports.devnull = function(endFn) {
var result = exports.writable({ objectMode: true });
if (endFn) {
result.once('finish', endFn);
}
return result;
};
exports.clone = function() {
return exports.map(cloneLib);
}
// Control flow
exports.fork = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments)),
result = through.obj();
args.forEach(function(target) {
// to avoid forked streams from interfering with each other, we will have to create a
// fresh clone for each fork
result.pipe(exports.clone()).pipe(target);
});
return result;
};
function trueFn() { return true; }
function parseMatch(args) {
var conditions = [],
streams = [],
i = 0;
while (i < args.length) {
if (typeof args[i] === 'function' && typeof args[i + 1] === 'object') {
conditions.push(args[i]);
streams.push(args[i + 1]);
i += 2;
} else { break; }
}
// the rest-stream is implemented as an appended stream with a condition that's always true
for (;i < args.length; i++) {
conditions.push(trueFn);
streams.push(args[i]);
}
return { conditions: conditions, streams: streams };
}
exports.match = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
return new Match(xtend({ objectMode: true }, parseMatch(args)));
};
exports.merge = require('merge-stream');
exports.forkMerge = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
return exports.combine(exports.fork(args), exports.merge(args));
};
exports.matchMerge = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments)),
parsed = xtend({ objectMode: true }, parseMatch(args));
return exports.combine(new Match(parsed), exports.merge(parsed.streams));
};
var miniq = require('miniq');
exports.parallel = function(limit, execFn, endFn) {
if (!execFn) {
execFn = function(task, enc, done) { task.call(this, done); };
}
var queue = miniq(limit),
cleanup = function(done) {
queue.removeAllListeners();
if (endFn) { endFn(done); } else { done(); }
},
stream = exports.thru.obj(function(chunk, enc, chunkDone) {
queue.exec(function(taskDone) {
execFn.call(stream, chunk, enc, taskDone);
});
if (!queue.isFull()) {
chunkDone(); // ask for more tasks, queue still has space
} else {
queue.once('done', function() { chunkDone(); }); // wait until a task completes
}
}, function(done) {
// once "_flush" occurs, wait for the queue to also become empty
if (queue.isEmpty()) {
cleanup(done);
} else {
queue.once('empty', cleanup.bind(this, done));
}
});
queue.on('done', stream.emit.bind(stream, 'done'));
queue.on('error', stream.emit.bind(stream, 'error'));
queue.on('empty', stream.emit.bind(stream, 'empty'));
return stream;
};
// Constructing pipelines from individual elements
exports.pipe = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
if (!isStream.isReadable(args[0])) {
throw new Error('pipe(): First stream must be readable.');
}
if (!isStream.isWritable(args[0])) {
throw new Error('pipe(): Last stream must be writable.');
}
args.slice(1, -1).map(function(stream) {
if (!isStream.isDuplex(stream)) {
throw new Error('pipe(): Streams in the pipeline must be duplex.');
}
});
args.reduce(function(prev, curr) { return prev.pipe(curr); });
return args;
}
exports.head = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
return exports.pipe(args)[0];
};
exports.tail = function() {
var args = (Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
return exports.pipe(args).pop();
};
exports.pipeline = function() {
var streams = exports.pipe((Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments)));
if (streams.length === 1) {
return streams[0];
}
var last = streams[streams.length - 1],
isDuplex = isStream.isDuplex(last),
head = isDuplex ? exports.combine(streams[0], last) : exports.cap(streams[0]);
// listen to errors in the middle streams (combine already listens to the first and last)
streams.slice(1, (isDuplex ? -1 : streams.length)).forEach(function(stream) {
stream.on('error', function(err) { head.emit('error', err); });
});
return head;
};
// isStream
exports.isStream = isStream;
exports.isReadable = isStream.isReadable;
exports.isWritable = isStream.isWritable;
exports.isDuplex = isStream.isDuplex;