import { withRetry } from "../src/retries"; const FAST_RETRY_CONFIG: Parameters[1] = { attempts: 6, // Always retry almost immediately, even when using real timers. backoffMs: () => 1, }; test("successful function only executes once", async () => { const fn = jest.fn(async () => "success!"); const result = await withRetry(fn, FAST_RETRY_CONFIG)(); expect(result).toEqual("success!"); expect(fn).toHaveBeenCalledTimes(1); }); test("failing function rejects after correct number of attempts", async () => { const fn = jest.fn(async () => { throw "oops!"; }); await expect(withRetry(fn, FAST_RETRY_CONFIG)()).rejects.toEqual("oops!"); expect(fn).toHaveBeenCalledTimes(FAST_RETRY_CONFIG.attempts); }); test("parameters are passed through", async () => { const fn = async (str: string, n: number) => { let result = ""; for (let i = 0; i < n; i += 1) { result += str; } return result; }; const retriable = withRetry(fn, FAST_RETRY_CONFIG); // A bit of type system hackery to assert at build time that function // signatures match. /* eslint-disable @typescript-eslint/no-unused-vars */ type Assert<_T extends true> = void; type _ = Assert; /* eslint-enable @typescript-eslint/no-unused-vars */ await expect(retriable("hello;", 3)).resolves.toEqual("hello;hello;hello;"); }); test("retries back off with expected delays", async () => { // Don't enable the `advanceTimers` configuration or else risk skewing the // timing scheme used in this test. jest.useFakeTimers(); let n = 0; const fn = jest.fn(async () => { if (n < 5) { n += 1; throw "oops!"; } else { // Fake timers don't play nicely with the `expect().rejects` API, so // ensure that the last attempt succeeds. return "success!"; } }); const promise = withRetry( fn, { attempts: 6, backoffMs: (attempt) => 1000 * 2 ** attempt, }, )(); expect(fn).toHaveBeenCalledTimes(1); await jest.advanceTimersByTimeAsync(100); expect(fn).toHaveBeenCalledTimes(1); await jest.advanceTimersByTimeAsync(1000); expect(fn).toHaveBeenCalledTimes(2); await jest.advanceTimersByTimeAsync(100); expect(fn).toHaveBeenCalledTimes(2); await jest.advanceTimersByTimeAsync(2000); expect(fn).toHaveBeenCalledTimes(3); await jest.advanceTimersByTimeAsync(100); expect(fn).toHaveBeenCalledTimes(3); await jest.advanceTimersByTimeAsync(4000); expect(fn).toHaveBeenCalledTimes(4); await jest.advanceTimersByTimeAsync(100); expect(fn).toHaveBeenCalledTimes(4); await jest.advanceTimersByTimeAsync(8000); expect(fn).toHaveBeenCalledTimes(5); await jest.advanceTimersByTimeAsync(100); expect(fn).toHaveBeenCalledTimes(5); await jest.advanceTimersByTimeAsync(16000); expect(fn).toHaveBeenCalledTimes(6); await expect(promise).resolves.toEqual("success!") // Mock timers without auto-advance enabled are a pain to manage when merely // counting retries, so use real timers and a very short backoff duration in // other tests instead. jest.useRealTimers(); });