advanced-retry
Version:
A retry library with advanced features
221 lines (220 loc) • 9.37 kB
JavaScript
;
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,
})));
});
}