UNPKG

advanced-retry

Version:
221 lines (220 loc) 9.37 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.abortSignalAny = abortSignalAny; exports.advancedRetry = advancedRetry; exports.advancedRetryAll = advancedRetryAll; function handleRetry(operation, errorResolvers, abortSignal) { return __awaiter(this, void 0, void 0, function* () { let resolverIndex = 0; let totalAttempts = 0; do { const currentResolver = resolverIndex < errorResolvers.length ? errorResolvers[resolverIndex] : undefined; resolverIndex++; let remainingAttempts = 1; let resolverRetries = 0; let context = { data: undefined }; do { if (abortSignal.aborted) { throw new Error('Operation aborted'); } try { totalAttempts++; const result = yield operation(context, abortSignal); return { result, totalAttempts, success: true, }; } catch (error) { if (abortSignal.aborted) { throw new Error('Operation aborted'); } /* istanbul ignore next */ if (!currentResolver || (remainingAttempts === 0 && resolverIndex >= errorResolvers.length)) { return { result: undefined, totalAttempts, error: error, success: false, }; } // Try each resolver in sequence until one returns a resolution const resolution = yield currentResolver({ error, attempt: resolverRetries, retryContext: context, abortSignal, }); context = resolution.context; if (resolution.unrecoverable) { return { result: undefined, totalAttempts, error: error, success: false, }; } remainingAttempts = resolution.remainingAttempts; resolverRetries++; if (remainingAttempts <= 0 && resolverIndex >= errorResolvers.length - 1) { return { result: undefined, totalAttempts, error: error, success: false, }; } } } while (remainingAttempts > 0); } while (resolverIndex < errorResolvers.length); /* istanbul ignore next */ throw new Error('Unexpected retry loop exit. This should never happen.'); }); } function abortSignalAny(abortSignals) { const abortController = new AbortController(); const abortListeners = []; abortSignals.forEach(s => { if (s != undefined) { const abortListener = () => { abortController.abort(); }; abortListeners.push(abortListener); s.addEventListener('abort', abortListener); if (s.aborted) { abortController.abort(); } } }); return { abortController, signal: abortController.signal, abortListeners }; } /** * Executes an operation with retry logic. * * @param operation - The operation to retry. * @param errorResolvers - The resolvers to use to try and recover * @param throwOnUnrecoveredError - Whether to throw an error if the operation failed to recover, instead of returning a result. * @returns The result of the operation */ function advancedRetry(_a) { return __awaiter(this, arguments, void 0, function* ({ operation: operation, errorResolvers = [], throwOnUnrecoveredError = false, overallTimeout = undefined, finallyCallback = undefined, abortSignal: externalAbortSignal = undefined, }) { const startTime = Date.now(); const timeoutController = new AbortController(); let timeoutAbortListener; let timeoutId; const { signal, abortListeners } = abortSignalAny([ timeoutController.signal, externalAbortSignal, ]); // Create a cleanup function const cleanup = () => { if (timeoutAbortListener) { signal.removeEventListener('abort', timeoutAbortListener); timeoutController.signal.removeEventListener('abort', timeoutAbortListener); } if (timeoutId) { clearTimeout(timeoutId); } abortListeners.forEach(l => signal.removeEventListener('abort', l)); // Ensure timeout controller is aborted if (!timeoutController.signal.aborted) { timeoutController.abort(); } }; try { const result = yield new Promise((resolve, reject) => { if (overallTimeout) { timeoutId = setTimeout(() => { timeoutController.abort(); reject(new Error('Operation timed out')); }, overallTimeout); timeoutAbortListener = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = undefined; } }; signal.addEventListener('abort', timeoutAbortListener); } handleRetry(operation, errorResolvers, signal) .then(v => { resolve(v); }) .catch(e => { reject(e); }); }); cleanup(); // Call cleanup before processing result if (result.success == false) { if (throwOnUnrecoveredError) { throw result.error; } return { success: false, error: result.error, totalAttemptsToSucceed: undefined, totalAttempts: result.totalAttempts, totalDurationMs: Date.now() - startTime, }; } return { success: true, result: result.result, totalAttemptsToSucceed: result.totalAttempts, totalAttempts: result.totalAttempts, totalDurationMs: Date.now() - startTime, }; } catch (error) { cleanup(); // Call cleanup before handling error if (throwOnUnrecoveredError) { throw error; } return { success: false, error: error, totalAttemptsToSucceed: undefined, totalAttempts: undefined, totalDurationMs: Date.now() - startTime, }; } finally { cleanup(); // Ensure cleanup runs in all cases finallyCallback === null || finallyCallback === void 0 ? void 0 : finallyCallback(); } }); } /** * Executes an operation with retry logic. * * @param operations - The operations to retry. All operations will be executed in parallel, if any of the operations fail, the operation will be retried. The other operations will continue to run. * @param errorResolvers - The resolvers to use to try and recover * @param overallTimeout - The overall timeout for the operation. If set and the operation takes longer than this, it will be cancelled, any retries will not be attempted. * @param abortSignal - An optional abort signal to cancel the operations if timeouts are used. * @returns The result of the operations */ function advancedRetryAll(_a) { return __awaiter(this, arguments, void 0, function* ({ operations, errorResolvers = [], overallTimeout = undefined, abortSignal = undefined, }) { return Promise.all(operations.map(operation => advancedRetry({ operation, errorResolvers, throwOnUnrecoveredError: false, overallTimeout, abortSignal, }))); }); }