youtube-maze/lib/retry.mjs

108 lines
2.6 KiB
JavaScript

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;
};
};