@xec-sh/core
Version:
Universal shell execution engine
94 lines • 3.8 kB
JavaScript
export class RetryError extends Error {
constructor(message, attempts, lastResult, results) {
super(message);
this.attempts = attempts;
this.lastResult = lastResult;
this.results = results;
this.name = 'RetryError';
}
}
export async function withExecutionRetry(fn, options = {}, eventEmitter) {
const { maxRetries = 3, initialDelay = 100, maxDelay = 30000, backoffMultiplier = 2, jitter = true, isRetryable = defaultIsRetryable, onRetry, } = options;
const results = [];
const startTime = Date.now();
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const result = await fn();
results.push(result);
if (result.exitCode === 0) {
if (attempt > 0 && eventEmitter) {
eventEmitter.emitEnhanced('retry:success', {
attempts: attempt + 1,
totalDuration: Date.now() - startTime,
command: result.command
}, 'retry');
}
return result;
}
if (attempt === maxRetries) {
if (eventEmitter) {
eventEmitter.emitEnhanced('retry:failed', {
attempts: maxRetries + 1,
totalDuration: Date.now() - startTime,
command: result.command,
lastError: result.stderr || `Exit code ${result.exitCode}`
}, 'retry');
}
throw new RetryError(`Failed after ${maxRetries + 1} attempts: ${result.command}`, maxRetries + 1, result, results);
}
if (!isRetryable(result)) {
if (eventEmitter) {
eventEmitter.emitEnhanced('retry:failed', {
attempts: attempt + 1,
totalDuration: Date.now() - startTime,
command: result.command,
lastError: 'Not retryable'
}, 'retry');
}
throw new RetryError(`Command failed and is not retryable: ${result.command}`, attempt + 1, result, results);
}
const baseDelay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt), maxDelay);
const delay = jitter ? addJitter(baseDelay) : baseDelay;
if (onRetry) {
onRetry(attempt + 1, result, delay);
}
if (eventEmitter) {
eventEmitter.emitEnhanced('retry:attempt', {
attempt: attempt + 1,
maxAttempts: maxRetries + 1,
delay,
command: result.command,
error: result.stderr || `Exit code ${result.exitCode}`
}, 'retry');
}
await sleep(delay);
}
throw new Error('Unexpected retry state');
}
function defaultIsRetryable(result) {
return result.exitCode !== 0;
}
function addJitter(delay) {
const jitterRange = delay * 0.25;
return delay + (Math.random() * 2 - 1) * jitterRange;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function createRetryableAdapter(adapter, defaultRetryOptions) {
return new Proxy(adapter, {
get(target, prop, receiver) {
if (prop === 'execute') {
return async (command) => {
const retryOptions = command.retry || defaultRetryOptions;
const maxRetries = retryOptions?.maxRetries ?? 0;
if (!retryOptions || maxRetries <= 0) {
return target.execute(command);
}
return withExecutionRetry(() => target.execute(command), retryOptions);
};
}
return Reflect.get(target, prop, receiver);
}
});
}
//# sourceMappingURL=retry-adapter.js.map