xaa
Version:
async/await/Promise helpers - delay, defer, timeout, each, map, filter
417 lines • 11.8 kB
JavaScript
/**
* @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