UNPKG

asyncbox

Version:

A collection of small async/await utilities

227 lines (214 loc) 6.66 kB
import B from 'bluebird'; import _ from 'lodash'; import type {LongSleepOptions, WaitForConditionOptions} from './types.js'; const LONG_SLEEP_THRESHOLD = 5000; // anything over 5000ms will turn into a spin /** * An async/await version of setTimeout * @param ms - The number of milliseconds to wait */ export async function sleep(ms: number): Promise<void> { return await B.delay(ms); } /** * Sometimes `Promise.delay` or `setTimeout` are inaccurate for large wait * times. To safely wait for these long times (e.g. in the 5+ minute range), you * can use `longSleep`. * * You can also pass a `progressCb` option which is a callback function that * receives an object with the properties `elapsedMs`, `timeLeft`, and * `progress`. This will be called on every wait interval so you can do your * wait logging or whatever. * @param ms - The number of milliseconds to wait * @param options - Options for controlling the long sleep behavior */ export async function longSleep( ms: number, {thresholdMs = LONG_SLEEP_THRESHOLD, intervalMs = 1000, progressCb = null}: LongSleepOptions = {}, ): Promise<void> { if (ms < thresholdMs) { return await sleep(ms); } const endAt = Date.now() + ms; let timeLeft: number; let elapsedMs = 0; do { const pre = Date.now(); await sleep(intervalMs); const post = Date.now(); timeLeft = endAt - post; elapsedMs = elapsedMs + (post - pre); if (_.isFunction(progressCb)) { progressCb({elapsedMs, timeLeft, progress: elapsedMs / ms}); } } while (timeLeft > 0); } /** * An async/await way of running a method until it doesn't throw an error * @param times - The maximum number of times to retry the function * @param fn - The async function to retry * @param args - Arguments to pass to the function */ export async function retry<T = any>( times: number, fn: (...args: any[]) => Promise<T>, ...args: any[] ): Promise<T | null> { let tries = 0; let done = false; let res: T | null = null; while (!done && tries < times) { tries++; try { res = await fn(...args); done = true; } catch (err) { if (tries >= times) { throw err; } } } return res; } /** * You can also use `retryInterval` to add a sleep in between retries. This can * be useful if you want to throttle how fast we retry. * @param times - The maximum number of times to retry the function * @param sleepMs - The number of milliseconds to wait between retries * @param fn - The async function to retry * @param args - Arguments to pass to the function */ export async function retryInterval<T = any>( times: number, sleepMs: number, fn: (...args: any[]) => Promise<T>, ...args: any[] ): Promise<T | null> { let count = 0; const wrapped = async (): Promise<T> => { count++; let res: T; try { res = await fn(...args); } catch (e) { // do not pause when finished the last retry if (count !== times) { await sleep(sleepMs); } throw e; } return res; }; return await retry(times, wrapped); } export const parallel = B.all; /** * Fire and forget async function execution * @param fn - The function to execute asynchronously * @param args - Arguments to pass to the function */ export function asyncify(fn: (...args: any[]) => any | Promise<any>, ...args: any[]): void { B.resolve(fn(...args)).done(); } /** * Similar to `Array.prototype.map`; runs in serial or parallel * @param coll - The collection to map over * @param mapper - The function to apply to each element * @param runInParallel - Whether to run operations in parallel (default: true) */ export async function asyncmap<T, R>( coll: T[], mapper: (value: T) => R | Promise<R>, runInParallel = true, ): Promise<R[]> { if (runInParallel) { return parallel(coll.map(mapper)); } const newColl: R[] = []; for (const item of coll) { newColl.push(await mapper(item)); } return newColl; } /** * Similar to `Array.prototype.filter` * @param coll - The collection to filter * @param filter - The function to test each element * @param runInParallel - Whether to run operations in parallel (default: true) */ export async function asyncfilter<T>( coll: T[], filter: (value: T) => boolean | Promise<boolean>, runInParallel = true, ): Promise<T[]> { const newColl: T[] = []; if (runInParallel) { const bools = await parallel(coll.map(filter)); for (let i = 0; i < coll.length; i++) { if (bools[i]) { newColl.push(coll[i]); } } } else { for (const item of coll) { if (await filter(item)) { newColl.push(item); } } } return newColl; } /** * Takes a condition (a function returning a boolean or boolean promise), and * waits until the condition is true. * * Throws a `/Condition unmet/` error if the condition has not been satisfied * within the allocated time, unless an error is provided in the options, as the * `error` property, which is either thrown itself, or used as the message. * * The condition result is returned if it is not falsy. If the condition throws an * error then this exception will be immediately passed through. * * The default options are: `{ waitMs: 5000, intervalMs: 500 }` * @param condFn - The condition function to evaluate * @param options - Options for controlling the wait behavior */ export async function waitForCondition<T>( condFn: () => Promise<T> | T, options: WaitForConditionOptions = {}, ): Promise<T> { const opts: WaitForConditionOptions & {waitMs: number; intervalMs: number} = _.defaults(options, { waitMs: 5000, intervalMs: 500, }); const debug = opts.logger ? opts.logger.debug.bind(opts.logger) : _.noop; const error = opts.error; const begunAt = Date.now(); const endAt = begunAt + opts.waitMs; const spin = async function spin(): Promise<T> { const result = await condFn(); if (result) { return result; } const now = Date.now(); const waited = now - begunAt; const remainingTime = endAt - now; if (now < endAt) { debug(`Waited for ${waited} ms so far`); await B.delay(Math.min(opts.intervalMs, remainingTime)); return await spin(); } // if there is an error option, it is either a string message or an error itself if (error) { throw _.isString(error) ? new Error(error) : error; } throw new Error(`Condition unmet after ${waited} ms. Timing out.`); }; return await spin(); } // Re-export types export type { Progress, ProgressCallback, LongSleepOptions, WaitForConditionOptions, } from './types.js';