@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
92 lines (91 loc) • 3.79 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformMap = void 0;
const js_lib_1 = require("@naturalcycles/js-lib");
const through2Concurrent = require("through2-concurrent");
const colors_1 = require("../../colors");
const stream_util_1 = require("../stream.util");
// doesn't work, cause here we don't construct our Transform instance ourselves
// export class TransformMap extends AbortableTransform {}
/**
* Like pMap, but for streams.
* Inspired by `through2`.
* Main feature is concurrency control (implemented via `through2-concurrent`) and convenient options.
* Using this allows native stream .pipe() to work and use backpressure.
*
* Only works in objectMode (due to through2Concurrent).
*
* Concurrency defaults to 16.
*
* If an Array is returned by `mapper` - it will be flattened and multiple results will be emitted from it. Tested by Array.isArray().
*/
function transformMap(mapper, opt = {}) {
const { concurrency = 16, predicate, // we now default to "no predicate" (meaning pass-everything)
errorMode = js_lib_1.ErrorMode.THROW_IMMEDIATELY, flattenArrayOutput, onError, metric = 'stream', logger = console, } = opt;
let index = -1;
let isSettled = false;
let errors = 0;
const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
return through2Concurrent.obj({
maxConcurrency: concurrency,
async final(cb) {
// console.log('transformMap final')
logErrorStats(true);
if (collectedErrors.length) {
// emit Aggregated error
cb(new js_lib_1.AggregatedError(collectedErrors));
}
else {
// emit no error
cb();
}
},
}, async function transformMapFn(chunk, _, cb) {
// Stop processing if isSettled (either THROW_IMMEDIATELY was fired or END received)
if (isSettled)
return cb();
const currentIndex = ++index;
try {
const res = await mapper(chunk, currentIndex);
const passedResults = await (0, js_lib_1.pFilter)(flattenArrayOutput && Array.isArray(res) ? res : [res], async (r) => {
if (r === js_lib_1.END) {
isSettled = true; // will be checked later
return false;
}
return r !== js_lib_1.SKIP && (!predicate || (await predicate(r, currentIndex)));
});
passedResults.forEach(r => this.push(r));
if (isSettled) {
logger.log(`transformMap END received at index ${currentIndex}`);
(0, stream_util_1.pipelineClose)('transformMap', this, this.sourceReadable, this.streamDone, logger);
}
cb(); // done processing
}
catch (err) {
logger.error(err);
errors++;
logErrorStats();
if (onError) {
try {
onError((0, js_lib_1._anyToError)(err), chunk);
}
catch { }
}
if (errorMode === js_lib_1.ErrorMode.THROW_IMMEDIATELY) {
isSettled = true;
return cb(err); // Emit error immediately
}
if (errorMode === js_lib_1.ErrorMode.THROW_AGGREGATED) {
collectedErrors.push(err);
}
// Tell input stream that we're done processing, but emit nothing to output - not error nor result
cb();
}
});
function logErrorStats(final = false) {
if (!errors)
return;
logger.log(`${metric} ${final ? 'final ' : ''}errors: ${(0, colors_1.yellow)(errors)}`);
}
}
exports.transformMap = transformMap;