import debug from 'debug'; import { sleep } from './util.mjs'; const log = debug('youtube-maze:retry'); /** * An error that is expected to be temporary. * * If this error is raised, the initial action may be retried after a short * period of time, and may eventually succeed. */ export class TemporaryError extends Error { constructor(message) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } } /** * Make an async function retry-able. * * When the underlying function throws a TemporaryError, the initial * call will be repeated, under the limits set below. * * @param function func Async function to call. * @param number retries Allowed number of retries before failing. * @param number cooldown Time to wait before retrying (ms). * @return function New function that is retryable. */ export const retryable = (func, retries = 3, cooldown = 1000) => { return async (...args) => { let remRetries = retries; let curCooldown = cooldown; while (true) { try { const result = await func(...args); return result; } catch (err) { if (err instanceof TemporaryError && remRetries > 0) { log(`\ ${func.name}(${args}) failed with error "${err.message}" Retrying in ${curCooldown} ms (${remRetries} retries remaining)`); await sleep(curCooldown); remRetries -= 1; curCooldown *= 2; } else { throw err; } } } }; }; /** * Make an async function mutually exclusive. * * Only one execution of the async function may happen at the same time. * In the meantime, other requests are added to a queue. * * @param function func Async function to call. * @param number cooldown Time to wait before two executions. */ export const exclusive = (func, cooldown = 2000) => { let pending = null; return async (...args) => { if (pending === null) { pending = func(...args); } else { pending = (async () => { try { await pending; } catch { // Ignore errors from previous executions } await sleep(cooldown); return func(...args); })(); } return pending; }; };