@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
129 lines (128 loc) • 4.66 kB
JavaScript
import { Transform } from 'node:stream';
import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error';
import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log';
import { END, SKIP } from '@naturalcycles/js-lib/types';
import { yellow } from '../../colors/colors.js';
import { PIPELINE_GRACEFUL_ABORT } from '../stream.util.js';
/**
* Sync (not async) version of transformMap.
* Supposedly faster, for cases when async is not needed.
*/
export function transformMapSync(mapper, opt = {}) {
const { predicate, // defaults to "no predicate" (pass everything)
errorMode = ErrorMode.THROW_IMMEDIATELY, onError, onDone, metric = 'stream', objectMode = true, signal, } = opt;
const started = Date.now();
let index = -1;
let countOut = 0;
let isSettled = false;
let errors = 0;
const collectedErrors = []; // only used if errorMode == THROW_AGGREGATED
const logger = createCommonLoggerAtLevel(opt.logger, opt.logLevel);
return new Transform({
objectMode,
highWaterMark: 1,
...opt,
transform(chunk, _, cb) {
// Stop processing if isSettled
if (isSettled)
return cb();
const currentIndex = ++index;
try {
// map and pass through
const v = mapper(chunk, currentIndex);
if (v === END) {
isSettled = true; // will be checked later
logger.log(`transformMapSync END received at index ${currentIndex}`);
_assert(signal, 'signal is required when using END');
signal.abort(new Error(PIPELINE_GRACEFUL_ABORT));
return cb();
}
if (v === SKIP) {
// do nothing, don't push
return cb();
}
if (!predicate || predicate(v, currentIndex)) {
countOut++;
this.push(v);
}
cb(); // done processing
}
catch (err) {
logger.error(err);
errors++;
logErrorStats();
if (onError) {
try {
onError(_anyToError(err), chunk);
}
catch { }
}
if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
isSettled = true;
try {
onDone?.({
ok: false,
collectedErrors,
countErrors: errors,
countIn: index + 1,
countOut,
started,
});
}
catch (err) {
logger.error(err);
}
// Emit error immediately
return cb(err);
}
if (errorMode === ErrorMode.THROW_AGGREGATED) {
collectedErrors.push(err);
}
cb();
}
},
final(cb) {
// console.log('transformMap final')
logErrorStats(true);
if (collectedErrors.length) {
try {
onDone?.({
ok: false,
collectedErrors,
countErrors: errors,
countIn: index + 1,
countOut,
started,
});
}
catch (err) {
logger.error(err);
}
// emit Aggregated error
cb(new AggregateError(collectedErrors, `transformMapSync resulted in ${collectedErrors.length} error(s)`));
}
else {
// emit no error
try {
onDone?.({
ok: true,
collectedErrors,
countErrors: errors,
countIn: index + 1,
countOut,
started,
});
}
catch (err) {
logger.error(err);
}
cb();
}
},
});
function logErrorStats(final = false) {
if (!errors)
return;
logger.log(`${metric} ${final ? 'final ' : ''}errors: ${yellow(errors)}`);
}
}