108 lines
2.6 KiB
JavaScript
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;
|
|
};
|
|
};
|