stream-chain
Version:
Chain functions as transform streams.
248 lines (210 loc) • 7.52 kB
JavaScript
// @ts-self-types="./index.d.ts"
;
const {Readable, Writable, Duplex} = require('node:stream');
const defs = require('./defs');
const gen = require('./gen');
const asStream = require('./asStream');
// is*NodeStream functions taken from https://github.com/nodejs/node/blob/master/lib/internal/streams/utils.js
const isReadableNodeStream = obj =>
obj &&
typeof obj.pipe === 'function' &&
typeof obj.on === 'function' &&
(!obj._writableState ||
(typeof obj._readableState === 'object' ? obj._readableState.readable : null) !== false) && // Duplex
(!obj._writableState || obj._readableState); // Writable has .pipe.
const isWritableNodeStream = obj =>
obj &&
typeof obj.write === 'function' &&
typeof obj.on === 'function' &&
(!obj._readableState ||
(typeof obj._writableState === 'object' ? obj._writableState.writable : null) !== false); // Duplex
const isDuplexNodeStream = obj =>
obj &&
typeof obj.pipe === 'function' &&
obj._readableState &&
typeof obj.on === 'function' &&
typeof obj.write === 'function';
const isReadableWebStream = obj =>
obj && globalThis.ReadableStream && obj instanceof globalThis.ReadableStream;
const isWritableWebStream = obj =>
obj && globalThis.WritableStream && obj instanceof globalThis.WritableStream;
const isDuplexWebStream = obj =>
obj &&
globalThis.ReadableStream &&
obj.readable instanceof globalThis.ReadableStream &&
globalThis.WritableStream &&
obj.writable instanceof globalThis.WritableStream;
const groupFunctions = (output, fn, index, fns) => {
if (
isDuplexNodeStream(fn) ||
(!index && isReadableNodeStream(fn)) ||
(index === fns.length - 1 && isWritableNodeStream(fn))
) {
output.push(fn);
return output;
}
if (isDuplexWebStream(fn)) {
output.push(Duplex.fromWeb(fn, {objectMode: true}));
return output;
}
if (!index && isReadableWebStream(fn)) {
output.push(Readable.fromWeb(fn, {objectMode: true}));
return output;
}
if (index === fns.length - 1 && isWritableWebStream(fn)) {
output.push(Writable.fromWeb(fn, {objectMode: true}));
return output;
}
if (typeof fn != 'function')
throw TypeError('Item #' + index + ' is not a proper stream, nor a function.');
if (!output.length) output.push([]);
const last = output[output.length - 1];
if (Array.isArray(last)) {
last.push(fn);
} else {
output.push([fn]);
}
return output;
};
const produceStreams = item => {
if (Array.isArray(item)) {
if (!item.length) return null;
if (item.length == 1) return item[0] && chain.asStream(item[0]);
return chain.asStream(chain.gen(...item));
}
return item;
};
const wrapFunctions = (fn, index, fns) => {
if (
isDuplexNodeStream(fn) ||
(!index && isReadableNodeStream(fn)) ||
(index === fns.length - 1 && isWritableNodeStream(fn))
) {
return fn; // an acceptable stream
}
if (isDuplexWebStream(fn)) {
return Duplex.fromWeb(fn, {objectMode: true});
}
if (!index && isReadableWebStream(fn)) {
return Readable.fromWeb(fn, {objectMode: true});
}
if (index === fns.length - 1 && isWritableWebStream(fn)) {
return Writable.fromWeb(fn, {objectMode: true});
}
if (typeof fn == 'function') return chain.asStream(fn); // a function
throw TypeError('Item #' + index + ' is not a proper stream, nor a function.');
};
// default implementation of required stream methods
const write = (input, chunk, encoding, callback) => {
let error = null;
try {
input.write(chunk, encoding, e => callback(e || error));
} catch (e) {
error = e;
}
};
const final = (input, callback) => {
let error = null;
try {
input.end(null, null, e => callback(e || error));
} catch (e) {
error = e;
}
};
const read = output => {
output.resume();
};
// the chain creator
const chain = (fns, options) => {
if (!Array.isArray(fns) || !fns.length) {
throw TypeError("Chain's first argument should be a non-empty array.");
}
fns = fns.flat(Infinity).filter(fn => fn);
const streams = (
options && options.noGrouping
? fns.map(wrapFunctions)
: fns
.map(fn => (defs.isFunctionList(fn) ? defs.getFunctionList(fn) : fn))
.flat(Infinity)
.reduce(groupFunctions, [])
.map(produceStreams)
).filter(s => s),
input = streams[0],
output = streams.reduce((output, item) => (output && output.pipe(item)) || item);
let stream = null; // will be assigned later
let writeMethod = (chunk, encoding, callback) => write(input, chunk, encoding, callback),
finalMethod = callback => final(input, callback),
readMethod = () => read(output);
if (!isWritableNodeStream(input)) {
writeMethod = (_1, _2, callback) => callback(null);
finalMethod = callback => callback(null);
input.on('end', () => stream.end());
}
if (isReadableNodeStream(output)) {
output.on('data', chunk => !stream.push(chunk) && output.pause());
output.on('end', () => stream.push(null));
} else {
readMethod = () => {}; // nop
output.on('finish', () => stream.push(null));
}
stream = new Duplex(
Object.assign({writableObjectMode: true, readableObjectMode: true}, options, {
readable: isReadableNodeStream(output),
writable: isWritableNodeStream(input),
write: writeMethod,
final: finalMethod,
read: readMethod
})
);
stream.streams = streams;
stream.input = input;
stream.output = output;
if (!isReadableNodeStream(output)) {
stream.resume();
}
// connect events
if (!options || !options.skipEvents) {
streams.forEach(item => item.on('error', error => stream.emit('error', error)));
}
return stream;
};
const dataSource = fn => {
if (typeof fn == 'function') return fn;
if (fn) {
if (typeof fn[Symbol.asyncIterator] == 'function') return fn[Symbol.asyncIterator].bind(fn);
if (typeof fn[Symbol.iterator] == 'function') return fn[Symbol.iterator].bind(fn);
}
throw new TypeError('The argument should be a function or an iterable object.');
};
module.exports = chain;
// from defs.js
module.exports.none = defs.none;
module.exports.stop = defs.stop;
module.exports.Stop = defs.Stop;
module.exports.finalSymbol = defs.finalSymbol;
module.exports.finalValue = defs.finalValue;
module.exports.final = defs.final;
module.exports.isFinalValue = defs.isFinalValue;
module.exports.getFinalValue = defs.getFinalValue;
module.exports.manySymbol = defs.manySymbol;
module.exports.many = defs.many;
module.exports.isMany = defs.isMany;
module.exports.getManyValues = defs.getManyValues;
module.exports.getFunctionList = defs.getFunctionList;
module.exports.flushSymbol = defs.flushSymbol;
module.exports.flushable = defs.flushable;
module.exports.isFlushable = defs.isFlushable;
module.exports.fListSymbol = defs.fListSymbol;
module.exports.isFunctionList = defs.isFunctionList;
module.exports.getFunctionList = defs.getFunctionList;
module.exports.setFunctionList = defs.setFunctionList;
module.exports.clearFunctionList = defs.clearFunctionList;
module.exports.toMany = defs.toMany;
module.exports.normalizeMany = defs.normalizeMany;
module.exports.combineMany = defs.combineMany;
module.exports.combineManyMut = defs.combineManyMut;
module.exports.chain = chain; // for compatibility with 2.x
module.exports.chainUnchecked = chain; // for TypeScript to bypass type checks
module.exports.gen = gen;
module.exports.asStream = asStream;
module.exports.dataSource = dataSource;