diff --git a/README.md b/README.md index d37b94f..488ff0e 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,22 @@ commonUtil.storage.set("user", { id: 1, name: "John" }); // Stores object in loc const user = commonUtil.storage.get<{ id: number; name: string }>("user"); // Retrieves typed object commonUtil.storage.remove("user"); // Removes item from localStorage +// Retry utilities +const result = await commonUtil.retry(async () => { + const response = await fetch("/api/data"); + if (!response.ok) throw new Error("API failed"); + return response.json(); +}, 3); // Retry up to 3 times + +// More retry examples +const userData = await commonUtil.retry(async () => { + return await fetchUserData(); +}); // Uses default 3 retries + +const fileUpload = await commonUtil.retry(async () => { + return await uploadFile(file); +}, 5); // Custom retry count + // Search Query utilities const queryParams = searchQueryUtil.getAllQuery(); // { key: ["value1", "value2"], id: "123" } @@ -193,6 +209,9 @@ storage.set("data", { key: "value" }); - πŸ›‘οΈ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data - πŸ”„ **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types +### Retry + +- `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Automatically re-attempts on error and returns the result of the first successful execution, or throws the last error if all retries fail. ### SearchQueryUtil - `getAllQuery(): Record` - Parses the current URL's query string and returns an object with key-value pairs. Values appear as arrays when the same key is used multiple times. diff --git a/package/commonUtil/index.ts b/package/commonUtil/index.ts index a163132..f965d2f 100644 --- a/package/commonUtil/index.ts +++ b/package/commonUtil/index.ts @@ -6,4 +6,5 @@ export { default as encodeBase64 } from "./encodeBase64"; export { default as decodeBase64 } from "./decodeBase64"; export { default as debounce } from "./debounce"; export { default as throttle } from "./throttle"; -export { default as storage } from "./storage"; +export { default as retry } from "./retry"; +export { default as storage } from "./storage"; \ No newline at end of file diff --git a/package/commonUtil/retry/index.test.ts b/package/commonUtil/retry/index.test.ts new file mode 100644 index 0000000..6eaf46f --- /dev/null +++ b/package/commonUtil/retry/index.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, vi } from "vitest"; +import retry from "."; + +describe("retry μœ ν‹Έ ν•¨μˆ˜ ν…ŒμŠ€νŠΈ", () => { + test("μ •μƒμ μœΌλ‘œ μ‹€ν–‰λ˜λŠ” ν•¨μˆ˜λŠ” ν•œλ²ˆλ§Œ μ‹€ν–‰ν•œλ‹€.", async () => { + const fn = vi.fn().mockResolvedValue("success!"); + + const result = await retry(fn, 3); + + expect(result).toBe("success!"); + expect(fn).toHaveBeenCalledTimes(1); + }); + + test("ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜λ©° 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ μž¬μ‹œλ„ν•œλ‹€.", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("fail 1")) + .mockRejectedValueOnce(new Error("fail 2")) + .mockResolvedValue("success!"); + + const result = await retry(fn, 3); + + expect(result).toBe("success!"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test("μž¬μ‹œλ„ ν•œλ„λ₯Ό μ΄ˆκ³Όν•˜λ©΄ λ§ˆμ§€λ§‰ μ—λŸ¬λ₯Ό λ˜μ§„λ‹€.", async () => { + const error = new Error("always fail"); + const fn = vi.fn().mockRejectedValue(error); + + await expect(retry(fn, 2)).rejects.toThrow("always fail"); + expect(fn).toHaveBeenCalledTimes(2); + }); + + test("κΈ°λ³Έ μž¬μ‹œλ„ νšŸμˆ˜λŠ” 3λ²ˆμ΄λ‹€.", async () => { + const fn = vi.fn().mockRejectedValue(new Error("fail")); + + await expect(retry(fn)).rejects.toThrow("fail"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test("νƒ€μž… μ•ˆμ „μ„±: λ°˜ν™˜ νƒ€μž…μ΄ μ˜¬λ°”λ₯΄κ²Œ μΆ”λ‘ λœλ‹€.", async () => { + const stringFn = vi.fn().mockResolvedValue("hello"); + const numberFn = vi.fn().mockResolvedValue(42); + const objectFn = vi.fn().mockResolvedValue({ id: 1 }); + + const stringResult = await retry(stringFn); + const numberResult = await retry(numberFn); + const objectResult = await retry(objectFn); + + expect(typeof stringResult).toBe("string"); + expect(typeof numberResult).toBe("number"); + expect(typeof objectResult).toBe("object"); + + expect(stringResult).toBe("hello"); + expect(numberResult).toBe(42); + expect(objectResult).toEqual({ id: 1 }); + }); + + test("0번 μž¬μ‹œλ„ μ‹œ ν•œ 번만 μ‹€ν–‰ν•œλ‹€.", async () => { + const fn = vi.fn().mockRejectedValue(new Error("fail")); + + await expect(retry(fn, 0)).rejects.toThrow("fail"); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/package/commonUtil/retry/index.ts b/package/commonUtil/retry/index.ts new file mode 100644 index 0000000..62a5c89 --- /dev/null +++ b/package/commonUtil/retry/index.ts @@ -0,0 +1,20 @@ +export default async function retry( + fn: () => Promise, + loop: number = 3 +): Promise { + let count = 0; + + const attempt = async (): Promise => { + try { + return await fn(); + } catch (e) { + count++; + if (count >= loop) { + throw e; + } + return attempt(); + } + }; + + return attempt(); +}