stream-chain
Version:
Chain functions, generators, Node streams, and Web streams into a pipeline with backpressure support.
217 lines (186 loc) • 6.04 kB
JavaScript
// @ts-self-types="./index.d.ts"
import {Readable, Writable, Duplex} from 'node:stream';
import * as defs from './defs.js';
import gen from './gen.js';
import asStream from './asStream.js';
import asWebStream from './asWebStream.js';
import dataSource from './dataSource.js';
const {
isReadableWebStream,
isWritableWebStream,
isDuplexWebStream,
isReadableNodeStream,
isWritableNodeStream,
isDuplexNodeStream
} = defs;
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] && /** @type {any} */ (chain).asStream(item[0]);
return /** @type {any} */ (chain).asStream(/** @type {any} */ (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 /** @type {any} */ (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?.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 = /** @type {Duplex & {streams: any[], input: any, output: any}} */ (
new Duplex({
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?.skipEvents) {
streams.forEach(item => item.on('error', error => stream.emit('error', error)));
}
return stream;
};
// from defs.js
chain.none = defs.none;
chain.stop = defs.stop;
chain.Stop = defs.Stop;
chain.finalSymbol = defs.finalSymbol;
chain.finalValue = defs.finalValue;
chain.final = defs.final;
chain.isFinalValue = defs.isFinalValue;
chain.getFinalValue = defs.getFinalValue;
chain.manySymbol = defs.manySymbol;
chain.many = defs.many;
chain.isMany = defs.isMany;
chain.getManyValues = defs.getManyValues;
chain.flushSymbol = defs.flushSymbol;
chain.flushable = defs.flushable;
chain.isFlushable = defs.isFlushable;
chain.fListSymbol = defs.fListSymbol;
chain.isFunctionList = defs.isFunctionList;
chain.getFunctionList = defs.getFunctionList;
chain.setFunctionList = defs.setFunctionList;
chain.clearFunctionList = defs.clearFunctionList;
chain.toMany = defs.toMany;
chain.normalizeMany = defs.normalizeMany;
chain.combineMany = defs.combineMany;
chain.combineManyMut = defs.combineManyMut;
chain.chain = chain; // for compatibility with 2.x
chain.chainUnchecked = chain; // for TypeScript to bypass type checks
chain.gen = gen;
chain.asStream = asStream;
chain.asWebStream = asWebStream;
chain.dataSource = dataSource;
export default chain;
export {chain, chain as chainUnchecked, gen, asStream, asWebStream, dataSource};
export * from './defs.js';