Skip to content

Instantly share code, notes, and snippets.

@toky-nomena
Last active August 8, 2024 14:31
Show Gist options
  • Save toky-nomena/850d52a0dc93461e54a6b68d7a6aada3 to your computer and use it in GitHub Desktop.
Save toky-nomena/850d52a0dc93461e54a6b68d7a6aada3 to your computer and use it in GitHub Desktop.
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);
});
});
@toky-nomena
Copy link
Author

toky-nomena commented Aug 8, 2024

import { retryPromise, RetryOptions, Predicate } from './retry';

describe('retryPromise', () => {
  test('succeeds on the first try', async () => {
    const promiseFactory = jest.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 = jest
      .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 = jest.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 () => {
    jest.useFakeTimers();
    
    const promiseFactory = jest
      .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);
    jest.advanceTimersByTime(100);
    const result = await retryPromisePromise;

    expect(result).toBe(42);
    expect(promiseFactory).toHaveBeenCalledTimes(2);
    jest.useRealTimers();
  });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment