UNPKG

@mdf.js/utils

Version:

MMS - API Core - Common utils tools

227 lines 8.68 kB
"use strict"; /** * 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