@mdf.js/utils
Version:
MMS - API Core - Common utils tools
227 lines • 8.68 kB
JavaScript
;
/**
* Copyright 2024 Mytra Control S.L. All rights reserved.
*
* Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
* or at https://opensource.org/licenses/MIT.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapOnRetry = exports.retry = exports.retryBind = exports.MAX_WAIT_TIME = exports.WAIT_TIME = void 0;
const tslib_1 = require("tslib");
const crash_1 = require("@mdf.js/crash");
const promises_1 = tslib_1.__importDefault(require("timers/promises"));
/**
* The wait time in milliseconds for retrying an operation.
*/
exports.WAIT_TIME = 100;
/**
* Maximum wait time in milliseconds for retrying an operation.
*/
exports.MAX_WAIT_TIME = 15000;
/** Default retry options for retrying operations */
const DEFAULT_RETRY_OPTIONS = {
logger: undefined,
waitTime: exports.WAIT_TIME,
maxWaitTime: exports.MAX_WAIT_TIME,
interrupt: undefined,
attempts: Number.MAX_SAFE_INTEGER,
actualAttempt: 1,
abortPromise: null,
timeout: undefined,
};
/**
* Auxiliar function to perform the wait process
* @param delay - time in milliseconds to wait for retry
* @returns
*/
const wait = (delay) => {
return promises_1.default.setTimeout(delay);
};
/**
* Log the error if there is an logger instance
* @param error - error to be logged
* @param loggerFunc - logger function
*/
const logging = (error, loggerFunc) => {
if (loggerFunc) {
loggerFunc(error);
}
};
/**
* Auxiliar function to create the watchdog promise for the timeout
* @param signal - signal to be used for the timeout
* @param attempts - actual function call attempts
* @param timeout - time in milliseconds to wait for retry
*/
const watchdog = (signal, attempts, timeout) => {
if (timeout === undefined) {
return null;
}
else {
return promises_1.default.setTimeout(timeout, undefined, { signal }).then(() => {
throw new crash_1.Crash(`The execution of the try number ${attempts} has timed out: ${timeout} ms`, {
name: 'TimeoutError',
info: { timeout },
});
});
}
};
/**
* Auxiliar function to create the abort promise
* @param attempts - actual function call attempts
* @param signal - signal to be used for the timeout
*/
const abortCancelSignal = (attempts, signal) => {
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
const cause = signal.reason ? crash_1.Crash.from(signal.reason) : undefined;
reject(new crash_1.Crash(`The task was aborted externally in attempt number: ${attempts}`, {
name: 'AbortError',
cause,
}));
}, { once: true });
});
};
/**
* Calculate the new wait time for the next call
* @param actualWaitTime - Actual wait time
* @param maxWaitTime - Max wait time
* @param actualAttempt - actual function call attempts
* @returns
*/
const calculateWaitTime = (actualWaitTime, maxWaitTime, actualAttempt) => {
return actualWaitTime + actualAttempt * exports.WAIT_TIME > maxWaitTime
? maxWaitTime
: actualWaitTime + actualAttempt * exports.WAIT_TIME;
};
/**
* Manage an error in one of the tries
* @param rawError - Error to be managed
* @param parameters - retry parameters
*/
async function errorManagement(rawError, parameters) {
const error = crash_1.Crash.from(rawError);
logging(error, parameters.logger);
if (error.name === 'InterruptionError' || error.name === 'AbortError') {
throw error;
}
if (parameters.interrupt && parameters.interrupt()) {
throw new crash_1.Crash(`The loop process was interrupted externally`, error.uuid, {
name: 'InterruptionError',
cause: error,
});
}
if (parameters.actualAttempt >= parameters.attempts) {
throw new crash_1.Crash(`Too much attempts [${parameters.actualAttempt}], the promise will not be retried`, error.uuid, { name: 'InterruptionError', cause: error });
}
if (error.name === 'IrresolvableError') {
throw new crash_1.Crash(`An irresolvable error was the cause of the interruption`, error.uuid, {
name: 'InterruptionError',
cause: error,
});
}
await wait(parameters.waitTime);
parameters.waitTime = calculateWaitTime(parameters.waitTime, parameters.maxWaitTime, parameters.actualAttempt);
parameters.actualAttempt += 1;
return parameters;
}
/**
* Perform the retry functionality for a promise
* @param task - promise to execute
* @param bindTo - instance to be binded to the task
* @param funcArgs - promise arguments
* @param options - control execution options
* @returns
*/
const retryBind = async (task, bindTo, funcArgs = [], options = {}) => {
if (typeof task !== 'function') {
throw new crash_1.Crash('The task must be a function', { name: 'TypeError' });
}
if (!Array.isArray(funcArgs)) {
throw new crash_1.Crash('The arguments must be an array', { name: 'TypeError' });
}
if (options.interrupt && options.abortSignal) {
throw new crash_1.Crash('The options `interrupt` and `abortSignal` are mutually exclusive, use only one of them', { name: 'ArgumentError' });
}
const parameters = { ...DEFAULT_RETRY_OPTIONS, ...options };
if (options.abortSignal && !parameters.abortPromise) {
if (!options.abortSignal.aborted) {
parameters.abortPromise = abortCancelSignal(parameters.actualAttempt, options.abortSignal);
}
else {
parameters.abortPromise = Promise.reject(new crash_1.Crash(`The task was aborted externally in attempt number: ${parameters.actualAttempt - 1}`, { name: 'AbortError' }));
}
}
const controller = new AbortController();
try {
const result = await Promise.race([
parameters.abortPromise,
watchdog(controller.signal, parameters.actualAttempt, parameters.timeout),
task.bind(bindTo).apply(task, funcArgs),
].filter(Boolean));
controller.abort();
return result;
}
catch (error) {
controller.abort();
return (0, exports.retryBind)(task, bindTo, funcArgs, await errorManagement(error, parameters));
}
};
exports.retryBind = retryBind;
/**
* Perform the retry functionality for a promise
* @param task - promise to execute
* @param funcArgs - promise arguments
* @param options - control execution options
* @returns
*/
const retry = async (task, funcArgs = [], options = {}) => {
if (typeof task !== 'function') {
throw new crash_1.Crash('The task must be a function', { name: 'TypeError' });
}
if (!Array.isArray(funcArgs)) {
throw new crash_1.Crash('The arguments must be an array', { name: 'TypeError' });
}
if (options.interrupt && options.abortSignal) {
throw new crash_1.Crash('The options `interrupt` and `abortSignal` are mutually exclusive, use only one of them', { name: 'ArgumentError' });
}
const parameters = { ...DEFAULT_RETRY_OPTIONS, ...options };
if (options.abortSignal && !parameters.abortPromise) {
if (!options.abortSignal.aborted) {
parameters.abortPromise = abortCancelSignal(parameters.actualAttempt, options.abortSignal);
}
else {
parameters.abortPromise = Promise.reject(new crash_1.Crash(`The task was aborted externally in attempt number: ${parameters.actualAttempt - 1}`, { name: 'AbortError' }));
}
}
const controller = new AbortController();
try {
const result = await Promise.race([
parameters.abortPromise,
watchdog(controller.signal, parameters.actualAttempt, parameters.timeout),
task.apply(task, funcArgs),
].filter(Boolean));
controller.abort();
return result;
}
catch (error) {
controller.abort();
return (0, exports.retry)(task, funcArgs, await errorManagement(error, parameters));
}
};
exports.retry = retry;
/**
* Wraps a task with retry functionality.
* @param task - The task to be executed.
* @param funcArgs - The arguments to be passed to the task.
* @param options - The options for retry behavior.
* @returns - A function that, when called, executes the task with retry.
*/
const wrapOnRetry = (task, funcArgs = [], options = {}) => {
const parameters = { ...DEFAULT_RETRY_OPTIONS, ...options };
return async () => {
return (0, exports.retry)(task, funcArgs, parameters);
};
};
exports.wrapOnRetry = wrapOnRetry;
//# sourceMappingURL=retry.js.map