UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

256 lines (214 loc) 6.8 kB
import { $pipeline, Alepha } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { $retry } from "../index.ts"; describe("$retry", () => { let alepha: Alepha; let time: DateTimeProvider; beforeEach(() => { alepha = Alepha.create(); time = alepha.inject(DateTimeProvider); }); afterEach(async () => { await alepha.stop(); }); test("should retry handler up to max retries", async () => { class Dummy { inc = 0; workRetry = $pipeline({ use: [$retry({ max: 3 })], handler: (n: number, end: number) => { this.inc += n; if (this.inc < end) { throw new Error("Retry"); } return this.inc; }, }); work = async (n: number, end: number) => { this.inc = 0; return await this.workRetry.run(n, end); }; } const basic = alepha.inject(Dummy); expect(await basic.work(1, 2)).toBe(2); expect(await basic.work(1, 3)).toBe(3); await expect(() => basic.work(1, 4)).rejects.toThrow(Error); }); test("should only retry when condition matches", async () => { class Dummy { inc = 0; workRetry = $pipeline({ use: [ $retry({ max: 10, when: (err: Error) => err.message === "Retry1", }), ], handler: (n: number, end: number) => { this.inc += n; if (this.inc < end) { throw new Error(`Retry${this.inc}`); } return this.inc; }, }); async work(n: number, end: number) { this.inc = 0; return await this.workRetry.run(n, end); } } const basic = alepha.inject(Dummy); expect(await basic.work(1, 2)).toBe(2); await expect(() => basic.work(1, 3)).rejects.toThrow(Error); }); test("should succeed on the first attempt", async () => { const handler = vi.fn().mockResolvedValue("success"); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [$retry()], handler, }); }, ).retry; await expect(retryFunc.run()).resolves.toBe("success"); expect(handler).toHaveBeenCalledTimes(1); }); test("should retry up to max attempts and then fail", async () => { const handler = vi.fn().mockRejectedValue(new Error("Failed")); const onError = vi.fn(); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [$retry({ max: 3, backoff: 0, onError })], handler, }); }, ).retry; await expect(retryFunc.run()).rejects.toThrow("Failed"); expect(handler).toHaveBeenCalledTimes(3); expect(onError).toHaveBeenCalledTimes(3); expect(onError).toHaveBeenCalledWith(expect.any(Error), 1); expect(onError).toHaveBeenCalledWith(expect.any(Error), 2); expect(onError).toHaveBeenCalledWith(expect.any(Error), 3); }); test("should succeed after a few failed attempts", async () => { let attempt = 0; const handler = vi.fn(() => { attempt++; if (attempt < 3) { return Promise.reject(new Error("Try again")); } return Promise.resolve("success"); }); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [$retry({ max: 4, backoff: 0 })], handler, }); }, ).retry; await expect(retryFunc.run()).resolves.toBe("success"); expect(handler).toHaveBeenCalledTimes(3); }); test("should respect maxDuration and time out", async () => { const handler = vi.fn(async () => { await time.wait(200); throw new Error("Failed"); }); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [$retry({ max: 5, maxDuration: [300, "ms"], backoff: 0 })], handler, }); }, ).retry; await expect(retryFunc.run()).rejects.toThrow( "Retry operation timed out after 300ms.", ); expect(handler).toHaveBeenCalledTimes(2); }); test("should be cancellable with an AbortSignal", async () => { const handler = vi.fn(async () => { await time.wait(500); // Long delay throw new Error("Failed"); }); const abortController = new AbortController(); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [ $retry({ max: 5, backoff: 100, signal: abortController.signal, }), ], handler, }); }, ).retry; const promise = retryFunc.run(); // Let the first attempt start await time.travel(100); // Abort during the first handler execution abortController.abort(); await expect(promise).rejects.toThrow("Retry operation was cancelled."); // Handler was called once but never completed expect(handler).toHaveBeenCalledTimes(1); }); test("should be cancellable by application shutdown", async () => { // Create a fresh alepha instance for this test const testAlepha = Alepha.create(); const handler = vi.fn(async () => { // Throw immediately to trigger retry throw new Error("Failed"); }); const retryFunc = testAlepha.inject( class { retry = $pipeline({ use: [$retry({ max: 10, backoff: 100 })], handler, }); }, ).retry; await testAlepha.start(); // Start the retry operation const promise = retryFunc.run(); // Give it a moment to start await new Promise((resolve) => setTimeout(resolve, 10)); // Stop the application which should abort all retries await testAlepha.stop(); // The promise should reject with cancellation error await expect(promise).rejects.toThrow("Retry operation was cancelled."); // Handler should have been called at least once but not 10 times expect(handler).toHaveBeenCalled(); expect(handler.mock.calls.length).toBeLessThan(10); }); test("should not retry if `when` condition returns false", async () => { class CustomError extends Error {} const handler = vi .fn() .mockRejectedValueOnce(new CustomError("Do not retry me")) .mockRejectedValue(new Error("Retry me")); const retryFunc = alepha.inject( class { retry = $pipeline({ use: [ $retry({ max: 3, backoff: 0, when: (error) => !(error instanceof CustomError), }), ], handler, }); }, ).retry; await expect(retryFunc.run()).rejects.toThrow(CustomError); expect(handler).toHaveBeenCalledTimes(1); }); });