UNPKG

async-transforms

Version:
223 lines (182 loc) 5.67 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var stream = require('stream'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var stream__default = /*#__PURE__*/_interopDefaultLegacy(stream); const filterSymbol = Symbol('filter'); /** * If returned by the map function, will skip this item in the final output. */ const skip = filterSymbol; /** * Build a mapping stream. This runs in parallel over receved chunks. * * Unlike the built-in Array.map function, returning null or undefined from the mapper will push * the same chunk onto the output. This acts more like forEach. * * By default, this operates in objectMode, and does not guarantee that the output order matches * the input order. * * @param {function(?, number): ?} handler * @param {Partial<import('.').Options>=} options * @return {!stream.Transform} */ function map(handler, options) { /** @type {import('.').Options} */ const o = Object.assign({ objectMode: true, order: false, tasks: 0, }, options); let index = 0; let count = 0; /** @type {stream.TransformCallback?} */ let flushCallback = null; o.tasks = Math.ceil(o.tasks) || 0; const hasTasks = o.tasks > 0; /** @type {{chunk: any, encoding: string}[]} */ const pending = []; let orderPushCount = 0; /** @type {any[]} */ const orderDone = []; const s = new stream__default['default'].Transform({ objectMode: o.objectMode, // nb. Passing writeableHighWaterMark here seems to do nothing, we just enforce tasks manually. transform(chunk, encoding, callback) { if (flushCallback !== null) { throw new Error(`got transform() after flush()`); } callback(); if (!hasTasks || count < o.tasks) { internalTransform(chunk); } else { pending.push({chunk, encoding}); } }, flush(callback) { if (count === 0) { callback(); // nothing was pushed, callback immediately } else { flushCallback = callback; } }, }); return s; // hoisted methods below /** * @param {any} chunk * @param {string} encoding */ function internalTransform(chunk, encoding) { ++count; const localIndex = index++; const resultHandler = internalResultHandler.bind(null, localIndex, chunk); Promise.resolve() .then(() => handler(chunk, localIndex)) .then(resultHandler) .catch((err) => s.destroy(err)); } /** * @param {number} localIndex * @param {any} chunk * @param {any} result */ function internalResultHandler(localIndex, chunk, result) { if (result == null) { result = chunk; // disallow null/undefined as they stop streams } if (o.order) { const doneIndex = localIndex - orderPushCount; orderDone[doneIndex] = result; // If we're the first, ship ourselves and any further completed chunks. if (doneIndex === 0) { let i = doneIndex; do { if (orderDone[i] !== filterSymbol) { s.push(orderDone[i]); } ++i; } while (i < orderDone.length && orderDone[i] !== undefined); // Splice at once, in case we hit many valid elements. orderDone.splice(0, i); orderPushCount += i; } } else if (result !== filterSymbol) { s.push(result); // we don't care about the order, push immediately } --count; if (pending.length && count < o.tasks) { const {chunk, encoding} = /** @type {typeof pending[0]} */ (pending.shift()); internalTransform(chunk); } else if (count === 0 && flushCallback) { // this is safe as `else if`, as calling internalTransform again means count > 0 flushCallback(); } } } /** * As per map, but returning falsey values will remove this from the stream. Returning a truthy * value will include it. * * @param {function(?, number): ?} handler * @param {Partial<import('.').Options>=} options * @return {!stream.Transform} */ function filter(handler, options) { return map(async (chunk, i) => { const result = await handler(chunk, i); return result ? chunk : filterSymbol; }, options); } /** * Asynchronously process all data passed through this stream prior to 'flush' being invoked. This * gates the throughput and pushes the array of returned values. * * This assumes object mode and does not validate or check encoding. * * @param {function(any[]): (Iterable<any>|Promise<Iterable<any>>)} handler * @return {!stream.Transform} */ function gate(handler) { /** @type {any[]} */ const chunks = []; return new stream__default['default'].Transform({ objectMode: true, transform(chunk, encoding, callback) { chunks.push(chunk); callback(); }, flush(callback) { Promise.resolve(handler(chunks)).then((result) => { if (result == null) { result = chunks; } // Technically, we allow anything iterable to be returned. for (const each of result) { this.push(each); } callback(); }).catch(callback); }, }); } /** * Returns a helper that generates an Array from piped data. This assumes object mode. */ function toArray() { let s; /** @type {Promise<any[]>} */ const promise = new Promise((resolve, reject) => { s = gate((arr) => { resolve(arr); return []; }); s.on('error', reject); }); return {stream: s, promise}; } exports.filter = filter; exports.gate = gate; exports.map = map; exports.skip = skip; exports.toArray = toArray;