289 lines
6.7 KiB
TypeScript
289 lines
6.7 KiB
TypeScript
export class RetryHelper {
|
|
static async retry<T>(
|
|
fn: () => Promise<T>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
backoff?: boolean;
|
|
onRetry?: (attempt: number, error: Error) => void;
|
|
} = {}
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
delay = 1000,
|
|
backoff = true,
|
|
onRetry
|
|
} = options;
|
|
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
if (attempt === maxAttempts) {
|
|
throw lastError;
|
|
}
|
|
|
|
if (onRetry) {
|
|
onRetry(attempt, lastError);
|
|
}
|
|
|
|
const currentDelay = backoff ? delay * attempt : delay;
|
|
await this.sleep(currentDelay);
|
|
}
|
|
}
|
|
|
|
throw lastError!;
|
|
}
|
|
|
|
static async retryWithCondition<T>(
|
|
fn: () => Promise<T>,
|
|
condition: (result: T) => boolean,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
timeout?: number;
|
|
onRetry?: (attempt: number, lastResult: T) => void;
|
|
} = {}
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 10,
|
|
delay = 500,
|
|
timeout = 10000,
|
|
onRetry
|
|
} = options;
|
|
|
|
const startTime = Date.now();
|
|
let lastResult: T | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
lastResult = await fn();
|
|
|
|
if (condition(lastResult)) {
|
|
return lastResult;
|
|
}
|
|
|
|
if (Date.now() - startTime > timeout) {
|
|
throw new Error(`Timeout after ${timeout}ms waiting for condition to be met`);
|
|
}
|
|
|
|
if (onRetry && lastResult !== undefined) {
|
|
onRetry(attempt, lastResult);
|
|
}
|
|
|
|
await this.sleep(delay);
|
|
} catch (error) {
|
|
if (Date.now() - startTime > timeout) {
|
|
throw new Error(`Timeout after ${timeout}ms: ${error}`);
|
|
}
|
|
|
|
await this.sleep(delay);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Condition not met after ${maxAttempts} attempts`);
|
|
}
|
|
|
|
static async retryElementAction<T>(
|
|
fn: () => Promise<T>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
ignoreErrors?: string[];
|
|
} = {}
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
delay = 1000,
|
|
ignoreErrors = ['Timeout', 'Element not found', 'Element not visible']
|
|
} = options;
|
|
|
|
return this.retry(fn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: true,
|
|
onRetry: (attempt, error) => {
|
|
const shouldIgnore = ignoreErrors.some(ignoredError =>
|
|
error.message.includes(ignoredError)
|
|
);
|
|
|
|
if (shouldIgnore) {
|
|
console.log(`Attempt ${attempt} failed with ignorable error: ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
static async retryNetworkRequest<T>(
|
|
fn: () => Promise<T>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
retryableStatuses?: number[];
|
|
} = {}
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
delay = 2000,
|
|
retryableStatuses = [408, 429, 500, 502, 503, 504]
|
|
} = options;
|
|
|
|
return this.retry(fn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: true,
|
|
onRetry: (attempt, error) => {
|
|
console.log(`Network request attempt ${attempt} failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
static async retryClick(
|
|
clickFn: () => Promise<void>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
} = {}
|
|
): Promise<void> {
|
|
const { maxAttempts = 3, delay = 500 } = options;
|
|
|
|
return this.retry(clickFn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: false,
|
|
onRetry: (attempt, error) => {
|
|
console.log(`Click attempt ${attempt} failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
static async retryFill(
|
|
fillFn: () => Promise<void>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
} = {}
|
|
): Promise<void> {
|
|
const { maxAttempts = 3, delay = 500 } = options;
|
|
|
|
return this.retry(fillFn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: false,
|
|
onRetry: (attempt, error) => {
|
|
console.log(`Fill attempt ${attempt} failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
static async retryNavigation(
|
|
navigateFn: () => Promise<void>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
} = {}
|
|
): Promise<void> {
|
|
const { maxAttempts = 3, delay = 1000 } = options;
|
|
|
|
return this.retry(navigateFn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: true,
|
|
onRetry: (attempt, error) => {
|
|
console.log(`Navigation attempt ${attempt} failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
static async retryAssertion<T>(
|
|
assertionFn: () => Promise<T>,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
} = {}
|
|
): Promise<T> {
|
|
const { maxAttempts = 5, delay = 500 } = options;
|
|
|
|
return this.retry(assertionFn, {
|
|
maxAttempts,
|
|
delay,
|
|
backoff: false,
|
|
onRetry: (attempt, error) => {
|
|
console.log(`Assertion attempt ${attempt} failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
static createRetryPolicy<T>(
|
|
fn: () => Promise<T>,
|
|
policy: {
|
|
maxAttempts: number;
|
|
initialDelay: number;
|
|
maxDelay?: number;
|
|
backoffMultiplier?: number;
|
|
retryCondition?: (error: Error) => boolean;
|
|
}
|
|
): () => Promise<T> {
|
|
const {
|
|
maxAttempts,
|
|
initialDelay,
|
|
maxDelay = 30000,
|
|
backoffMultiplier = 2,
|
|
retryCondition
|
|
} = policy;
|
|
|
|
return async () => {
|
|
let currentDelay = initialDelay;
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
if (retryCondition && !retryCondition(lastError)) {
|
|
throw lastError;
|
|
}
|
|
|
|
if (attempt === maxAttempts) {
|
|
throw lastError;
|
|
}
|
|
|
|
console.log(`Attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`);
|
|
await this.sleep(currentDelay);
|
|
currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
|
|
}
|
|
}
|
|
|
|
throw lastError!;
|
|
};
|
|
}
|
|
|
|
static async retryWithTimeout<T>(
|
|
fn: () => Promise<T>,
|
|
timeout: number,
|
|
options: {
|
|
maxAttempts?: number;
|
|
delay?: number;
|
|
} = {}
|
|
): Promise<T> {
|
|
const { maxAttempts = 3, delay = 1000 } = options;
|
|
|
|
return Promise.race([
|
|
this.retry(fn, { maxAttempts, delay }),
|
|
new Promise<T>((_, reject) =>
|
|
setTimeout(() => reject(new Error(`Operation timed out after ${timeout}ms`)), timeout)
|
|
)
|
|
]);
|
|
}
|
|
}
|