Last active
August 8, 2024 14:31
-
-
Save toky-nomena/850d52a0dc93461e54a6b68d7a6aada3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Predicate<T> = (result: T) => boolean; | |
interface RetryOptions { | |
retries: number; | |
delay?: number; // optional delay between retries in milliseconds | |
} | |
async function retryPromise<T>( | |
promiseFactory: () => Promise<T>, | |
predicate: Predicate<T>, | |
options: RetryOptions | |
): Promise<T> { | |
let attempts = 0; | |
while (attempts < options.retries) { | |
try { | |
const result = await promiseFactory(); | |
if (predicate(result)) { | |
return result; | |
} | |
} catch (error) { | |
if (attempts >= options.retries - 1) { | |
throw error; | |
} | |
} | |
attempts++; | |
if (options.delay) { | |
await new Promise((resolve) => setTimeout(resolve, options.delay)); | |
} | |
} | |
throw new Error('Retries exhausted'); | |
} | |
import { describe, test, expect, vi } from 'vitest'; | |
import { retryPromise, RetryOptions, Predicate } from './path-to-your-file'; | |
describe('retryPromise', () => { | |
test('succeeds on the first try', async () => { | |
const promiseFactory = vi.fn().mockResolvedValue(42); | |
const predicate: Predicate<number> = (result) => result === 42; | |
const options: RetryOptions = { retries: 3 }; | |
const result = await retryPromise(promiseFactory, predicate, options); | |
expect(result).toBe(42); | |
expect(promiseFactory).toHaveBeenCalledTimes(1); | |
}); | |
test('retries until success', async () => { | |
const promiseFactory = vi | |
.fn() | |
.mockRejectedValueOnce('Error') | |
.mockRejectedValueOnce('Error') | |
.mockResolvedValueOnce(42); | |
const predicate: Predicate<number> = (result) => result === 42; | |
const options: RetryOptions = { retries: 3 }; | |
const result = await retryPromise(promiseFactory, predicate, options); | |
expect(result).toBe(42); | |
expect(promiseFactory).toHaveBeenCalledTimes(3); | |
}); | |
test('fails after exhausting retries', async () => { | |
const promiseFactory = vi.fn().mockResolvedValue(24); | |
const predicate: Predicate<number> = (result) => result === 42; | |
const options: RetryOptions = { retries: 3 }; | |
await expect(retryPromise(promiseFactory, predicate, options)).rejects.toThrow('Retries exhausted'); | |
expect(promiseFactory).toHaveBeenCalledTimes(3); | |
}); | |
test('respects delay between retries', async () => { | |
vi.useFakeTimers(); | |
const promiseFactory = vi | |
.fn() | |
.mockRejectedValueOnce('Error') | |
.mockResolvedValueOnce(42); | |
const predicate: Predicate<number> = (result) => result === 42; | |
const options: RetryOptions = { retries: 2, delay: 100 }; | |
const retryPromisePromise = retryPromise(promiseFactory, predicate, options); | |
vi.advanceTimersByTime(100); | |
const result = await retryPromisePromise; | |
expect(result).toBe(42); | |
expect(promiseFactory).toHaveBeenCalledTimes(2); | |
vi.useRealTimers(); | |
}); | |
test('fails after all retries if promise always rejects', async () => { | |
const promiseFactory = vi.fn().mockRejectedValue(new Error('Always fails')); | |
const predicate: Predicate<number> = () => true; // Will not matter as promise always rejects | |
const options: RetryOptions = { retries: 3 }; | |
await expect(retryPromise(promiseFactory, predicate, options)).rejects.toThrow('Always fails'); | |
expect(promiseFactory).toHaveBeenCalledTimes(3); | |
}); | |
}); |
Author
toky-nomena
commented
Aug 8, 2024
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment