UNPKG

xaa

Version:

async/await/Promise helpers - delay, defer, timeout, each, map, filter

417 lines 11.8 kB
/** * @packageDocumentation * @module index */ /* eslint-disable max-statements, @typescript-eslint/ban-types, max-params, complexity */ import assert from "assert"; /** * check if something is a promise * * @param p * @returns true or false */ export function isPromise(p) { return p && p.then && p.catch && typeof p.then === "function" && typeof p.catch === "function"; } /** * Defer object for fulfilling a promise later in other events * * To use, use `xaa.makeDefer` or its alias `xaa.defeer`. * */ export class Defer { /** * construct Defer * * @param ThePromise optional promise constructor */ constructor(ThePromise = global.Promise) { this.promise = new ThePromise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); // Not declaring (err, result) explicitly for standard node.js callback. // we can't be sure if user's API expects callback that // should have a second arg for result. this.done = (err, ...args) => { if (err) this.reject(err); else this.resolve(args[0]); }; } } /** * Create a promise Defer object * * Sample: * * ```js * async function waitEvent() { * const defer = xaa.makeDefer(); * someThing.on("event", (data) => defer.resolve(data)) * return defer.promise; * } * ``` * * @param ThePromise - optional Promise constructor. * @returns Defer instance */ export function makeDefer(ThePromise = global.Promise) { return new Defer(ThePromise); } export { makeDefer as defer }; /** * The error xaa.timeout will throw if operation timed out */ export class TimeoutError extends Error { constructor(msg) { super(msg); this.name = "TimeoutError"; assert(msg); } } export async function delay(delayMs, valOrFunc) { await new Promise(resolve => setTimeout(resolve, delayMs)); return typeof valOrFunc === "function" ? /* lazily */ valOrFunc() : valOrFunc; } /** * TimeoutRunner for running tasks (promises) with a timeout * * Please use `xaa.timeout` or `xaa.runTimeout` APIs instead. */ export class TimeoutRunner { /** * constructor * @param maxMs - number of milliseconds to allow tasks to run * @param rejectMsg - message to reject with when timeout triggers * @param options - TimeoutRunnerOptions */ constructor(maxMs, rejectMsg, options) { this.maxMs = maxMs; this.rejectMsg = rejectMsg; this.defer = makeDefer(options.Promise); this.ThePromise = options.Promise; this.TimeoutError = options.TimeoutError; this.timeout = setTimeout(() => this.defer.reject(new this.TimeoutError(rejectMsg)), maxMs); } /** * check if runner has failed with error * * @returns has error flag */ hasError() { return this.hasOwnProperty("error"); } /** * check if runner has finished with result * * @returns has result flag */ hasResult() { return this.hasOwnProperty("result"); } /** * Run tasks * * @param tasks - Promise or function that returns Promise, or array of them. * @returns Promise to wait for tasks to complete, or timeout error. */ async run(tasks) { const process = async (x) => (typeof x === "function" ? x() : x); // Cast below is due in part to https://github.com/microsoft/TypeScript/issues/17002 const arrTasks = !Array.isArray(tasks) ? process(tasks) : this.ThePromise.all(tasks.map(process)); try { const r = await this.ThePromise.race([arrTasks, this.defer.promise]); this.clear(); this.result = r; return r; } catch (err) { this.clear(); this.error = err; throw err; } } /** * Cancel the operation and reject with msg * * @param msg - cancel message */ cancel(msg = "xaa TimeoutRunner operation cancelled") { this.clear(); this.defer.reject(new this.TimeoutError(msg)); } /** Explicitly clear the setTimeout handle */ clear() { if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } } /** * Check if runner is done * * @returns is done flag */ isDone() { return this.hasResult() || this.hasError(); } } /** * Create a TimeoutRunner to run tasks (promises) with timeout * * Sample: * * ```js * await xaa.timeout(1000, "timeout fetching data").run(() => fetch(url)) * ``` * * with promises: * * ```js * await xaa.timout(1000).run([promise1, promise2]) * ``` * * @param maxMs - number of milliseconds to allow tasks to run * @param rejectMsg - message to reject with when timeout triggers * @returns TimeoutRunner object */ export function timeout(maxMs, rejectMsg = "xaa TimeoutRunner operation timed out", options = { TimeoutError: TimeoutError, Promise: global.Promise }) { return new TimeoutRunner(maxMs, rejectMsg, options); } export async function runTimeout(tasks, maxMs, rejectMsg, options) { return await timeout(maxMs, rejectMsg, options).run(tasks); } /** * create map context * * @param array - array to map * @returns map context */ function createMapContext(array) { return { array, failed: false, assertNoFailure() { if (this.failed) { throw new Error("assertNoFailure"); } } }; } /** * async map for array that supports concurrency * * Use by xaa.map internally. * * @param array array to map, if any item is promise-like, it will be resolved first. * @param func mapper callback * @param options MapOptions * @returns promise with mapped result */ function multiMap(array, func, options) { const awaited = new Array(array.length); let error; let completedCount = 0; let freeSlots = options.concurrency; let index = 0; const iterator = Symbol.iterator in array && typeof array[Symbol.iterator] === "function" ? array[Symbol.iterator]() : null; let totalCount = iterator ? Infinity : array.length; const context = createMapContext(array); const defer = makeDefer(); const fail = (err) => { context.failed = true; if (!error) { error = err; // Safe because of the following line: error.partial = awaited; defer.reject(error); } }; const mapNext = () => { // important to check this here, so an empty input array immediately // gets resolved with an empty result. if (!error && completedCount === totalCount) { return defer.resolve(awaited); } if (error || freeSlots <= 0 || index >= totalCount) { return null; } const ir = iterator && iterator.next(); /* c8 ignore next 7 */ if (ir && ir.done) { if (totalCount !== index) { totalCount = index; return mapNext(); } return null; } freeSlots--; const pendingIx = index++; const save = (x) => { completedCount++; freeSlots++; awaited[pendingIx] = x; mapNext(); }; const handleRet = res => { if (isPromise(res)) { res.then(save, fail); return mapNext(); } else { return save(res); } }; const item = ir ? ir.value : array[pendingIx]; if (isPromise(item)) { return item.then(val => { return handleRet(func.call(options.thisArg, val, pendingIx, context)); }, fail); } else { try { return handleRet(func.call(options.thisArg, item, pendingIx, context)); } catch (err) { return fail(err); } } }; // // Should not use setTimeout for next: // // Top level code in async functions before any await statements, execute synchronously. // // Similar to that setting up promises is sync, and then the // fulfilment of them is async, meaning their .then are called. // mapNext(); return defer.promise; } export async function mapSeries(array, func, options) { const awaited = new Array(); const context = createMapContext(array); let i = 0; try { for (const element of array) { const item = isPromise(element) ? await element : element; awaited[i] = await func.call(options && options.thisArg, item, i, context); i++; } } catch (err) { context.failed = true; err.partial = awaited; throw err; } return awaited; } /** * async map array with concurrency * * - intended to be similar to `bluebird.map` * * @param array array to map, if any item is promise-like, it will be resolved first. * @param func - callback to map values from the array * @param options - MapOptions * @returns promise with mapped result */ export async function map(array, func, options = { concurrency: 50 }) { assert(Array.isArray(array), `xaa.map expecting an array but got ${typeof array}`); if (options.concurrency > 1) { return multiMap(array, func, options); } else { return mapSeries(array, func, options); } } /** * async version of array.forEach * - iterate through array and await call func with each element and index * * Sample: * * ```js * await xaa.each([1, 2, 3], async val => await xaa.delay(val)) * ``` * * @param array array to each * @param func callback for each */ export async function each(array, func) { let i = 0; const items = []; for (const element of array) { const item = isPromise(element) ? await element : element; items.push(item); await func(item, i); i++; } return items; } /** * async filter array * * Sample: * * ```js * await xaa.filter([1, 2, 3], async val => await validateResult(val)) * ``` * * Beware: concurrency is fixed to 1. * * @param array array to filter * @param func callback for filter * @returns filtered result */ export async function filter(array, func) { const filtered = []; for (let i = 0; i < array.length; i++) { const x = await func(array[i], i); if (x) filtered.push(array[i]); } return filtered; } /** * try to: * - await a promise * - call and await function that returns a promise * * If exception occur, then return `valOrFunc` * * - if `valOrFunc` is a function, then return `valOrFunc(err)` * * @param funcOrPromise function or promise to try * @param valOrFunc value, or callback to get value, to return if `func` throws * @returns result, `valOrFunc`, or `valOrFunc(err)`. */ export async function tryCatch(funcOrPromise, valOrFunc) { try { if (isPromise(funcOrPromise)) { return await funcOrPromise; } return await funcOrPromise(); } catch (err) { return typeof valOrFunc === "function" ? valOrFunc(err) : valOrFunc; } } export { tryCatch as try }; /** * Wrap the calling of a function into async/await (promise) context * - intended to be similar to `bluebird.try` * * @param func function to wrap in async context * @param args arguments to pass to `func` * @returns result from `func` */ export async function wrap(func, ...args2) { return func(...args2); } //# sourceMappingURL=index.js.map