UNPKG

asyncbox

Version:

A collection of small async/await utilities

269 lines 8.57 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.longSleep = exports.waitForCondition = exports.asyncfilter = exports.asyncmap = exports.parallel = exports.asyncify = exports.retryInterval = exports.nodeifyAll = exports.nodeify = exports.retry = exports.sleep = void 0; const bluebird_1 = __importDefault(require("bluebird")); const lodash_1 = __importDefault(require("lodash")); const LONG_SLEEP_THRESHOLD = 5000; // anything over 5000ms will turn into a spin /** * An async/await version of setTimeout * @param {number} ms * @returns {Promise<void>} */ async function sleep(ms) { return await bluebird_1.default.delay(ms); } exports.sleep = sleep; /** * 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`. * * sYou 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 {number} ms * @param {LongSleepOptions} [opts] * @returns {Promise<void>} */ async function longSleep(ms, { thresholdMs = LONG_SLEEP_THRESHOLD, intervalMs = 1000, progressCb = null, } = {}) { if (ms < thresholdMs) { return await sleep(ms); } const endAt = Date.now() + ms; let timeLeft; let elapsedMs = 0; do { const pre = Date.now(); await sleep(intervalMs); const post = Date.now(); timeLeft = endAt - post; elapsedMs = elapsedMs + (post - pre); if (lodash_1.default.isFunction(progressCb)) { progressCb({ elapsedMs, timeLeft, progress: elapsedMs / ms }); } } while (timeLeft > 0); } exports.longSleep = longSleep; /** * An async/await way of running a method until it doesn't throw an error * @template [T=any] * @param {number} times * @param {(...args: any[]) => Promise<T>} fn * @param {...any} args * @returns {Promise<T?>} */ async function retry(times, fn, ...args) { let tries = 0; let done = false; let res = null; while (!done && tries < times) { tries++; try { res = await fn(...args); done = true; } catch (err) { if (tries >= times) { throw err; } } } return res; } exports.retry = retry; /** * 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. * @template [T=any] * @param {number} times * @param {number} sleepMs * @param {(...args: any[]) => Promise<T>} fn * @param {...any} args * @returns {Promise<T?>} */ async function retryInterval(times, sleepMs, fn, ...args) { let count = 0; let wrapped = async () => { count++; let res; 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); } exports.retryInterval = retryInterval; const parallel = bluebird_1.default.all; exports.parallel = parallel; /** * Export async functions (Promises) and import this with your ES5 code to use * it with Node. * @template [R=any] * @param {any} promisey * @param {(err: any, value?: R) => void} cb * @returns {Promise<R>} */ function nodeify(promisey, cb) { return bluebird_1.default.resolve(promisey).nodeify(cb); } exports.nodeify = nodeify; /** * Node-ify an entire object of `Promise`-returning functions * @param {Record<string,(...args: any[]) => any>} promiseyMap * @returns {Record<string,(...args: any[])=>void>} */ function nodeifyAll(promiseyMap) { /** @type {Record<string,(...args: any[])=>void>} */ let cbMap = {}; for (const [name, fn] of lodash_1.default.toPairs(promiseyMap)) { cbMap[name] = function (...args) { const _cb = args.slice(-1)[0]; args = args.slice(0, -1); nodeify(fn(...args), _cb); }; } return cbMap; } exports.nodeifyAll = nodeifyAll; /** * @param {(...args: any[]) => any|Promise<any>} fn * @param {...any} args */ function asyncify(fn, ...args) { bluebird_1.default.resolve(fn(...args)).done(); } exports.asyncify = asyncify; /** * Similar to `Array.prototype.map`; runs in serial * @param {any[]} coll * @param {(value: any) => any|Promise<any>} mapper * @returns {Promise<any[]>} */ async function asyncmap(coll, mapper, runInParallel = true) { if (runInParallel) { return parallel(coll.map(mapper)); } let newColl = []; for (let item of coll) { newColl.push(await mapper(item)); } return newColl; } exports.asyncmap = asyncmap; /** * Similar to `Array.prototype.filter` * @param {any[]} coll * @param {(value: any) => any|Promise<any>} filter * @param {boolean} runInParallel * @returns {Promise<any[]>} */ async function asyncfilter(coll, filter, runInParallel = true) { let newColl = []; if (runInParallel) { let bools = await parallel(coll.map(filter)); for (let i = 0; i < coll.length; i++) { if (bools[i]) { newColl.push(coll[i]); } } } else { for (let item of coll) { if (await filter(item)) { newColl.push(item); } } } return newColl; } exports.asyncfilter = asyncfilter; /** * 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 }` * @template T * @param {() => Promise<T>|T} condFn * @param {WaitForConditionOptions} [options] * @returns {Promise<T>} */ async function waitForCondition(condFn, options = {}) { /** @type {WaitForConditionOptions & {waitMs: number, intervalMs: number}} */ const opts = lodash_1.default.defaults(options, { waitMs: 5000, intervalMs: 500, }); const debug = opts.logger ? opts.logger.debug.bind(opts.logger) : lodash_1.default.noop; const error = opts.error; const begunAt = Date.now(); const endAt = begunAt + opts.waitMs; /** @returns {Promise<T>} */ const spin = async function spin() { 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 bluebird_1.default.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 throw error ? (lodash_1.default.isString(error) ? new Error(error) : error) : new Error(`Condition unmet after ${waited} ms. Timing out.`); }; return await spin(); } exports.waitForCondition = waitForCondition; /** * Options for {@link waitForCondition} * @typedef WaitForConditionOptions * @property {number} [waitMs] * @property {number} [intervalMs] * @property {{debug: (...args: any[]) => void}} [logger] * @property {string|Error} [error] */ /** * Options for {@link longSleep} * @typedef LongSleepOptions * @property {number} [thresholdMs] * @property {number} [intervalMs] * @property {ProgressCallback?} [progressCb] */ /** * Parameter provided to a {@link ProgressCallback} * @typedef Progress * @property {number} elapsedMs * @property {number} timeLeft * @property {number} progress */ /** * Progress callback for {@link longSleep} * @callback ProgressCallback * @param {Progress} progress * @returns {void} */ //# sourceMappingURL=asyncbox.js.map