From 01b13d6a65599cb85e863dffed1ebfbea140e74b Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 28 Jul 2025 14:35:51 +0700 Subject: [PATCH 1/8] feat(core/stdlib): enhance retry function with customizable options and add tests --- core/stdlib/src/async/index.ts | 1 + core/stdlib/src/async/retry/index.test.ts | 250 ++++++++++++++++++++++ core/stdlib/src/async/retry/index.ts | 63 +++++- core/stdlib/src/async/tryIt/index.test.ts | 24 +-- core/stdlib/src/async/tryIt/index.ts | 16 +- 5 files changed, 327 insertions(+), 27 deletions(-) create mode 100644 core/stdlib/src/async/retry/index.test.ts diff --git a/core/stdlib/src/async/index.ts b/core/stdlib/src/async/index.ts index 1d96a50..d4e70f3 100644 --- a/core/stdlib/src/async/index.ts +++ b/core/stdlib/src/async/index.ts @@ -1,2 +1,3 @@ +export * from './retry'; export * from './sleep'; export * from './tryIt'; diff --git a/core/stdlib/src/async/retry/index.test.ts b/core/stdlib/src/async/retry/index.test.ts new file mode 100644 index 0000000..3fb7131 --- /dev/null +++ b/core/stdlib/src/async/retry/index.test.ts @@ -0,0 +1,250 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { retry } from '.'; + +describe('retry', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('return the result on first successful attempt', async () => { + const successFn = vi.fn().mockResolvedValue('success'); + + const result = await retry(successFn); + + expect(result).toBe('success'); + expect(successFn).toHaveBeenCalledTimes(1); + expect(successFn).toHaveBeenCalledWith({ count: 1, stop: expect.any(Function) }); + }); + + it('use default times value of 2', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + await expect(retry(failingFn)).rejects.toThrow('Test error'); + + expect(failingFn).toHaveBeenCalledTimes(2); + }); + + it('retry the specified number of times on failure', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + await expect(retry(failingFn, { times: 3 })).rejects.toThrow('Test error'); + + expect(failingFn).toHaveBeenCalledTimes(3); + expect(failingFn).toHaveBeenNthCalledWith(1, { count: 1, stop: expect.any(Function) }); + expect(failingFn).toHaveBeenNthCalledWith(2, { count: 2, stop: expect.any(Function) }); + expect(failingFn).toHaveBeenNthCalledWith(3, { count: 3, stop: expect.any(Function) }); + }); + + it('succeed on the last attempt', async () => { + const partiallyFailingFn = vi.fn() + .mockRejectedValueOnce(new Error('First failure')) + .mockRejectedValueOnce(new Error('Second failure')) + .mockResolvedValue('success'); + + const result = await retry(partiallyFailingFn, { times: 3 }); + + expect(result).toBe('success'); + expect(partiallyFailingFn).toHaveBeenCalledTimes(3); + }); + + it('use custom shouldRetry function', async () => { + const networkError = new Error('Network failed'); + networkError.name = 'NetworkError'; + const failingFn = vi.fn().mockRejectedValue(networkError); + + await expect(retry(failingFn, { + times: 3, + shouldRetry: (error) => error.name !== 'NetworkError' + })).rejects.toThrow('Network failed'); + + expect(failingFn).toHaveBeenCalledTimes(1); + }); + + it('retry with custom shouldRetry based on count', async () => { + const testError = new Error('Test error'); + const failingFn = vi.fn().mockRejectedValue(testError); + + await expect(retry(failingFn, { + times: 5, + shouldRetry: (error, count) => count < 3 // Only retry first 2 attempts + })).rejects.toThrow('Test error'); + + expect(failingFn).toHaveBeenCalledTimes(3); // Initial + 2 retries + }); + + it('retry specific error types with custom shouldRetry', async () => { + const temporaryError = new Error('Temporary failure'); + temporaryError.name = 'TemporaryError'; + const permanentError = new Error('Permanent failure'); + permanentError.name = 'PermanentError'; + + const failingFn = vi.fn() + .mockRejectedValueOnce(temporaryError) + .mockRejectedValueOnce(temporaryError) + .mockRejectedValueOnce(permanentError); + + await expect(retry(failingFn, { + times: 5, + shouldRetry: (error) => error.name === 'TemporaryError' + })).rejects.toThrow('Permanent failure'); + + expect(failingFn).toHaveBeenCalledTimes(3); + }); + + it('wait for the specified delay between retries', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + const retryPromise = retry(failingFn, { times: 3, delay: 1000 }); + + // First call should happen immediately + expect(failingFn).toHaveBeenCalledTimes(1); + + // Advance time to trigger first retry + await vi.advanceTimersByTimeAsync(1000); + expect(failingFn).toHaveBeenCalledTimes(2); + + // Advance time to trigger second retry + await vi.advanceTimersByTimeAsync(1000); + expect(failingFn).toHaveBeenCalledTimes(3); + + await expect(retryPromise).rejects.toThrow('Test error'); + }); + + it('use dynamic delay function', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + const delayFn = vi.fn((count: number) => count * 500); + + const retryPromise = retry(failingFn, { times: 3, delay: delayFn }); + + // First call should happen immediately + expect(failingFn).toHaveBeenCalledTimes(1); + + // First retry should wait for delay(2) = 1000ms + await vi.advanceTimersByTimeAsync(1000); + expect(failingFn).toHaveBeenCalledTimes(2); + expect(delayFn).toHaveBeenCalledWith(2); + + // Second retry should wait for delay(3) = 1500ms + await vi.advanceTimersByTimeAsync(1500); + expect(failingFn).toHaveBeenCalledTimes(3); + expect(delayFn).toHaveBeenCalledWith(3); + + await expect(retryPromise).rejects.toThrow('Test error'); + }); + + it('not delay after the last attempt', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + const retryPromise = retry(failingFn, { times: 2, delay: 1000 }); + + // Wait for the first retry delay + await vi.advanceTimersByTimeAsync(1000); + + // Should complete without further delays + await expect(retryPromise).rejects.toThrow('Test error'); + expect(failingFn).toHaveBeenCalledTimes(2); + }); + + it('handle zero delay', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + await expect(retry(failingFn, { times: 3, delay: 0 })).rejects.toThrow('Test error'); + + expect(failingFn).toHaveBeenCalledTimes(3); + }); + + it('pass the count parameter to the function', async () => { + const countingFn = vi.fn(async ({ count }: { count: number }) => { + if (count < 3) { + throw new Error(`Attempt ${count} failed`); + } + return `Success on attempt ${count}`; + }); + + const result = await retry(countingFn, { times: 3 }); + + expect(result).toBe('Success on attempt 3'); + expect(countingFn).toHaveBeenCalledWith({ count: 1, stop: expect.any(Function) }); + expect(countingFn).toHaveBeenCalledWith({ count: 2, stop: expect.any(Function) }); + expect(countingFn).toHaveBeenCalledWith({ count: 3, stop: expect.any(Function) }); + }); + + it('throw the last error when all attempts fail', async () => { + const firstError = new Error('First error'); + const lastError = new Error('Last error'); + const failingFn = vi.fn() + .mockRejectedValueOnce(firstError) + .mockRejectedValueOnce(lastError); + + await expect(retry(failingFn, { times: 2 })).rejects.toThrow('Last error'); + }); + + it('handle times value of 1', async () => { + const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); + + await expect(retry(failingFn, { times: 1 })).rejects.toThrow('Test error'); + + expect(failingFn).toHaveBeenCalledTimes(1); + }); + + it('handle function that returns non-promise values', async () => { + const syncFn = vi.fn(async ({ count }: { count: number }) => { + if (count === 1) { + throw new Error('First attempt failed'); + } + return 'success'; + }); + + const result = await retry(syncFn, { times: 2 }); + + expect(result).toBe('success'); + expect(syncFn).toHaveBeenCalledTimes(2); + }); + + it('handle complex return types', async () => { + const complexFn = vi.fn().mockResolvedValue({ + data: [1, 2, 3], + status: 'ok', + metadata: { timestamp: 123456 } + }); + + const result = await retry(complexFn); + + expect(result).toEqual({ + data: [1, 2, 3], + status: 'ok', + metadata: { timestamp: 123456 } + }); + }); + + it('stop retrying when stop function is called', async () => { + const customError = new Error('Custom stop error'); + const stopFn = vi.fn(async ({ count, stop }: { count: number, stop: (error: any) => void }) => { + if (count === 2) { + stop(customError); + } + throw new Error(`Attempt ${count} failed`); + }); + + await expect(retry(stopFn, { times: 5 })).rejects.toThrow('Custom stop error'); + + expect(stopFn).toHaveBeenCalledTimes(2); + }); + + it('stop retrying with undefined error when stop is called without argument', async () => { + const stopFn = vi.fn(async ({ count, stop }: { count: number, stop: (error?: any) => void }) => { + if (count === 2) { + stop(); + } + throw new Error(`Attempt ${count} failed`); + }); + + await expect(retry(stopFn, { times: 5 })).rejects.toBeUndefined(); + + expect(stopFn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/core/stdlib/src/async/retry/index.ts b/core/stdlib/src/async/retry/index.ts index f9aadf8..450880a 100644 --- a/core/stdlib/src/async/retry/index.ts +++ b/core/stdlib/src/async/retry/index.ts @@ -1,9 +1,22 @@ +import { tryIt } from '../tryIt'; +import { sleep } from '../sleep'; +import { isFunction } from '../../types'; + export interface RetryOptions { times?: number; - delay?: number; - backoff: (options: RetryOptions & { count: number }) => number; + delay?: number | ((count: number) => number); + shouldRetry?: (error: Error, count: number) => boolean; } +export type RetryFunction = ( + args: { + count: number; + stop: (error: any) => void; + }, +) => Promise; + +const RetryEarlyExit = Symbol('RetryEarlyExit'); + /** * @name retry * @category Async @@ -27,12 +40,48 @@ export interface RetryOptions { * */ export async function retry( - fn: () => Promise, - options: RetryOptions -) { + fn: RetryFunction, + options: RetryOptions = {}, +): Promise { const { - times = 3, + times = 2, + delay = 0, + shouldRetry, } = options; - let count = 0; + let count = 1; + let lastError: Error = new Error('Retry failed'); + + while (count <= times) { + const metadata = { + count, + stop: (error?: any) => { + throw { [RetryEarlyExit]: error }; + }, + }; + + const { error, data } = await tryIt(fn)(metadata); + + if (!error) + return data; + + if (RetryEarlyExit in error) + throw error[RetryEarlyExit]; + + if (shouldRetry && !shouldRetry(error, count)) + throw error; + + lastError = error; + count++; + + // Don't delay after the last attempt + if (count <= times) { + const delayMs = isFunction(delay) ? delay(count) : delay; + + if (delayMs > 0) + await sleep(delayMs); + } + } + + throw lastError; } diff --git a/core/stdlib/src/async/tryIt/index.test.ts b/core/stdlib/src/async/tryIt/index.test.ts index c84da67..25d668a 100644 --- a/core/stdlib/src/async/tryIt/index.test.ts +++ b/core/stdlib/src/async/tryIt/index.test.ts @@ -6,62 +6,62 @@ describe('tryIt', () => { const syncFn = (x: number) => x * 2; const wrappedSyncFn = tryIt(syncFn); - const [error, result] = wrappedSyncFn(2); + const { error, data } = wrappedSyncFn(2); expect(error).toBeUndefined(); - expect(result).toBe(4); + expect(data).toBe(4); }); it('handle synchronous functions with errors', () => { const syncFn = (): void => { throw new Error('Test error') }; const wrappedSyncFn = tryIt(syncFn); - const [error, result] = wrappedSyncFn(); + const { error, data } = wrappedSyncFn(); expect(error).toBeInstanceOf(Error); expect(error?.message).toBe('Test error'); - expect(result).toBeUndefined(); + expect(data).toBeUndefined(); }); it('handle asynchronous functions without errors', async () => { const asyncFn = async (x: number) => x * 2; const wrappedAsyncFn = tryIt(asyncFn); - const [error, result] = await wrappedAsyncFn(2); + const { error, data } = await wrappedAsyncFn(2); expect(error).toBeUndefined(); - expect(result).toBe(4); + expect(data).toBe(4); }); it('handle asynchronous functions with errors', async () => { const asyncFn = async () => { throw new Error('Test error') }; const wrappedAsyncFn = tryIt(asyncFn); - const [error, result] = await wrappedAsyncFn(); + const { error, data } = await wrappedAsyncFn(); expect(error).toBeInstanceOf(Error); expect(error?.message).toBe('Test error'); - expect(result).toBeUndefined(); + expect(data).toBeUndefined(); }); it('handle promise-based functions without errors', async () => { const promiseFn = (x: number) => Promise.resolve(x * 2); const wrappedPromiseFn = tryIt(promiseFn); - const [error, result] = await wrappedPromiseFn(2); + const { error, data } = await wrappedPromiseFn(2); expect(error).toBeUndefined(); - expect(result).toBe(4); + expect(data).toBe(4); }); it('handle promise-based functions with errors', async () => { const promiseFn = () => Promise.reject(new Error('Test error')); const wrappedPromiseFn = tryIt(promiseFn); - const [error, result] = await wrappedPromiseFn(); + const { error, data } = await wrappedPromiseFn(); expect(error).toBeInstanceOf(Error); expect(error?.message).toBe('Test error'); - expect(result).toBeUndefined(); + expect(data).toBeUndefined(); }); }); \ No newline at end of file diff --git a/core/stdlib/src/async/tryIt/index.ts b/core/stdlib/src/async/tryIt/index.ts index 9b4d9ba..e43ab21 100644 --- a/core/stdlib/src/async/tryIt/index.ts +++ b/core/stdlib/src/async/tryIt/index.ts @@ -1,8 +1,8 @@ import { isPromise } from '../../types'; export type TryItReturn = Return extends Promise - ? Promise<[Error, undefined] | [undefined, Awaited]> - : [Error, undefined] | [undefined, Return]; + ? Promise<{ error: Error; data: undefined } | { error: undefined; data: Awaited }> + : { error: Error; data: undefined } | { error: undefined; data: Return }; /** * @name tryIt @@ -14,10 +14,10 @@ export type TryItReturn = Return extends Promise * * @example * const wrappedFetch = tryIt(fetch); - * const [error, result] = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1'); + * const { error, data } = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1'); * * @example - * const [error, result] = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1'); + * const { error, data } = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1'); * * @since 0.0.3 */ @@ -30,12 +30,12 @@ export function tryIt( if (isPromise(result)) return result - .then((value) => [undefined, value]) - .catch((error) => [error, undefined]) as TryItReturn; + .then((value) => ({ error: undefined, data: value })) + .catch((error) => ({ error, data: undefined })) as TryItReturn; - return [undefined, result] as TryItReturn; + return { error: undefined, data: result } as TryItReturn; } catch (error) { - return [error, undefined] as TryItReturn; + return { error, data: undefined } as TryItReturn; } }; } From a61fb85088af320db2d48a7ed8836988d1d7d580 Mon Sep 17 00:00:00 2001 From: robonen Date: Mon, 28 Jul 2025 14:42:30 +0700 Subject: [PATCH 2/8] docs(core/stdlib): update @since annotation for clarity --- core/stdlib/src/async/retry/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/stdlib/src/async/retry/index.ts b/core/stdlib/src/async/retry/index.ts index 450880a..a2a099f 100644 --- a/core/stdlib/src/async/retry/index.ts +++ b/core/stdlib/src/async/retry/index.ts @@ -38,6 +38,7 @@ const RetryEarlyExit = Symbol('RetryEarlyExit'); * .then(response => response.json()) * }, { times: 3, delay: 1000 }); * + * @since 0.0.8 */ export async function retry( fn: RetryFunction, From 49d70f1f39f29e1c67a5d6bf1175351b5df2369c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:32:12 +0000 Subject: [PATCH 3/8] chore(deps): update pnpm/action-setup action to v5 --- .github/workflows/ci.yaml | 2 +- .github/workflows/publish.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b2233aa..5752467 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 with: run_install: false diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0675e34..3ac6de2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -18,7 +18,7 @@ jobs: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 with: run_install: false From 7c5c2ab9c16c6f949fb36109a5060302d8279810 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:00:39 +0000 Subject: [PATCH 4/8] chore(deps): update pnpm.catalog.default jsdom to v29 --- pnpm-lock.yaml | 183 +++++++++++++++++++++++--------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 95 insertions(+), 90 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63a8529..801d6ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ catalogs: specifier: ^2.4.6 version: 2.4.6 jsdom: - specifier: ^28.0.0 - version: 28.0.0 + specifier: ^29.0.1 + version: 29.0.1 oxlint: specifier: ^1.2.0 version: 1.47.0 @@ -52,13 +52,13 @@ importers: version: 2.6.1 jsdom: specifier: 'catalog:' - version: 28.0.0 + version: 29.0.1 scule: specifier: ^1.3.0 version: 1.3.0 vitest: specifier: 'catalog:' - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2) configs/oxlint: devDependencies: @@ -164,17 +164,16 @@ importers: packages: - '@acemir/cssom@0.9.31': - resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} - '@arcanis/slice-ansi@1.1.1': resolution: {integrity: sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w==} - '@asamuzakjp/css-color@4.1.2': - resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@asamuzakjp/dom-selector@6.7.8': - resolution: {integrity: sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==} + '@asamuzakjp/dom-selector@7.0.4': + resolution: {integrity: sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -434,6 +433,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@breejs/later@4.2.0': resolution: {integrity: sha512-EVMD0SgJtOuFeg0lAVbCwa+qeTKILb87jqvLyUtQswGD9+ce2nB52Y5zbTF1Hc0MDFfbydcMcxb47jSdhikVHA==} engines: {node: '>= 10'} @@ -441,8 +444,8 @@ packages: '@cdktf/hcl2json@0.21.0': resolution: {integrity: sha512-cwX3i/mSJI/cRrtqwEPRfawB7pXgNioriSlkvou8LWiCrrcDe9ZtTbAbu8W1tEJQpe1pnX9VEgpzf/BbM7xF8Q==} - '@csstools/color-helpers@6.0.1': - resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} '@csstools/css-calc@3.1.1': @@ -452,8 +455,8 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.0.1': - resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==} + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -465,8 +468,13 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.0.27': - resolution: {integrity: sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==} + '@csstools/css-syntax-patches-for-csstree@1.1.1': + resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true '@csstools/css-tokenizer@4.0.0': resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} @@ -637,8 +645,8 @@ packages: cpu: [x64] os: [win32] - '@exodus/bytes@1.14.1': - resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: '@noble/hashes': ^1.8.0 || ^2.0.0 @@ -2051,18 +2059,14 @@ packages: css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - cssstyle@5.3.7: - resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} - engines: {node: '>=20'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2464,6 +2468,7 @@ packages: git-raw-commits@2.0.11: resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} engines: {node: '>=10'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true git-up@8.1.1: @@ -2787,9 +2792,9 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsdom@28.0.0: - resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + jsdom@29.0.1: + resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -2887,14 +2892,14 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.1: - resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==} - engines: {node: 20 || >=22} - lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -2969,8 +2974,8 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -3451,6 +3456,7 @@ packages: prebuild-install@7.1.2: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prettier@3.6.2: @@ -3877,6 +3883,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser@5.44.0: resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} @@ -3936,8 +3943,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} tr46@0.0.3: @@ -4041,8 +4048,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.21.0: - resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + undici@7.24.5: + resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==} engines: {node: '>=20.18.1'} unicorn-magic@0.3.0: @@ -4215,8 +4222,8 @@ packages: resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} - whatwg-url@16.0.0: - resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} whatwg-url@5.0.0: @@ -4316,27 +4323,25 @@ packages: snapshots: - '@acemir/cssom@0.9.31': {} - '@arcanis/slice-ansi@1.1.1': dependencies: grapheme-splitter: 1.0.4 - '@asamuzakjp/css-color@4.1.2': + '@asamuzakjp/css-color@5.0.1': dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.6 + lru-cache: 11.2.7 - '@asamuzakjp/dom-selector@6.7.8': + '@asamuzakjp/dom-selector@7.0.4': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 - css-tree: 3.1.0 + css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.6 + lru-cache: 11.2.7 '@asamuzakjp/nwsapi@2.3.9': {} @@ -5247,22 +5252,26 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@breejs/later@4.2.0': {} '@cdktf/hcl2json@0.21.0': dependencies: fs-extra: 11.3.0 - '@csstools/color-helpers@6.0.1': {} + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 6.0.1 + '@csstools/color-helpers': 6.0.2 '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -5271,7 +5280,9 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.0.27': {} + '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 '@csstools/css-tokenizer@4.0.0': {} @@ -5369,7 +5380,7 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@exodus/bytes@1.14.1': {} + '@exodus/bytes@1.15.0': {} '@gwhitney/detect-indent@7.0.1': {} @@ -5445,7 +5456,7 @@ snapshots: agent-base: 7.1.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.1 + lru-cache: 11.2.6 socks-proxy-agent: 8.0.3 transitivePeerDependencies: - supports-color @@ -6406,7 +6417,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -6451,7 +6462,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2) '@vitest/utils@4.0.18': dependencies: @@ -6729,7 +6740,7 @@ snapshots: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 glob: 13.0.1 - lru-cache: 11.2.1 + lru-cache: 11.2.6 minipass: 7.1.2 minipass-collect: 2.0.1 minipass-flush: 1.0.5 @@ -6859,20 +6870,13 @@ snapshots: domutils: 3.1.0 nth-check: 2.1.1 - css-tree@3.1.0: + css-tree@3.2.1: dependencies: - mdn-data: 2.12.2 + mdn-data: 2.27.1 source-map-js: 1.2.1 css-what@6.1.0: {} - cssstyle@5.3.7: - dependencies: - '@asamuzakjp/css-color': 4.1.2 - '@csstools/css-syntax-patches-for-csstree': 1.0.27 - css-tree: 3.1.0 - lru-cache: 11.2.6 - csstype@3.2.3: {} dargs@7.0.0: {} @@ -6882,7 +6886,7 @@ snapshots: data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.0 + whatwg-url: 16.0.1 transitivePeerDependencies: - '@noble/hashes' @@ -7436,7 +7440,7 @@ snapshots: html-encoding-sniffer@6.0.0: dependencies: - '@exodus/bytes': 1.14.1 + '@exodus/bytes': 1.15.0 transitivePeerDependencies: - '@noble/hashes' @@ -7450,6 +7454,7 @@ snapshots: debug: 4.4.1 transitivePeerDependencies: - supports-color + optional: true http2-wrapper@1.0.3: dependencies: @@ -7620,31 +7625,31 @@ snapshots: jsbn@1.1.0: optional: true - jsdom@28.0.0: + jsdom@29.0.1: dependencies: - '@acemir/cssom': 0.9.31 - '@asamuzakjp/dom-selector': 6.7.8 - '@exodus/bytes': 1.14.1 - cssstyle: 5.3.7 + '@asamuzakjp/css-color': 5.0.1 + '@asamuzakjp/dom-selector': 7.0.4 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 data-urls: 7.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.7 parse5: 8.0.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 6.0.0 - undici: 7.21.0 + tough-cookie: 6.0.1 + undici: 7.24.5 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.0 + whatwg-url: 16.0.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' - - supports-color jsesc@3.1.0: {} @@ -7727,10 +7732,10 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.1: {} - lru-cache@11.2.6: {} + lru-cache@11.2.7: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -7889,7 +7894,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdn-data@2.12.2: {} + mdn-data@2.27.1: {} mdurl@2.0.0: {} @@ -8460,7 +8465,7 @@ snapshots: path-scurry@2.0.0: dependencies: - lru-cache: 11.2.1 + lru-cache: 11.2.6 minipass: 7.1.2 pathe@2.0.3: {} @@ -9211,7 +9216,7 @@ snapshots: totalist@3.0.1: {} - tough-cookie@6.0.0: + tough-cookie@6.0.1: dependencies: tldts: 7.0.23 @@ -9306,7 +9311,7 @@ snapshots: undici-types@7.16.0: {} - undici@7.21.0: {} + undici@7.24.5: {} unicorn-magic@0.3.0: {} @@ -9401,7 +9406,7 @@ snapshots: terser: 5.44.0 yaml: 2.8.2 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2)) @@ -9427,7 +9432,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@types/node': 24.10.13 '@vitest/ui': 4.0.18(vitest@4.0.18) - jsdom: 28.0.0 + jsdom: 29.0.1 transitivePeerDependencies: - jiti - less @@ -9465,9 +9470,9 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.0: + whatwg-url@16.0.1: dependencies: - '@exodus/bytes': 1.14.1 + '@exodus/bytes': 1.15.0 tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f4491fd..1eed83b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,7 @@ packages: catalog: '@vitest/coverage-v8': ^4.0.18 '@vue/test-utils': ^2.4.6 - jsdom: ^28.0.0 + jsdom: ^29.0.1 oxlint: ^1.2.0 tsdown: ^0.12.5 vitest: ^4.0.18 From 8d8ea734d11ab1c6341a89f6302731420cd272e0 Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 26 Mar 2026 06:09:54 +0700 Subject: [PATCH 5/8] refactor(core/stdlib): improve retry function implementation and error handling --- core/stdlib/src/async/retry/index.ts | 50 ++++++++++++++++------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/core/stdlib/src/async/retry/index.ts b/core/stdlib/src/async/retry/index.ts index a2a099f..1ac6ccb 100644 --- a/core/stdlib/src/async/retry/index.ts +++ b/core/stdlib/src/async/retry/index.ts @@ -9,35 +9,40 @@ export interface RetryOptions { } export type RetryFunction = ( - args: { + args: { count: number; stop: (error: any) => void; }, ) => Promise; -const RetryEarlyExit = Symbol('RetryEarlyExit'); +class RetryEarlyExitError { + cause: any; + constructor(cause: any) { + this.cause = cause; + } +} /** * @name retry * @category Async * @description Retries a function a specified number of times with a delay between each retry - * + * * @param {Promise} fn - The function to retry * @param {RetryOptions} options - The options for the retry * @returns {Promise} - The result of the function - * + * * @example * const result = await retry(() => { * return fetch('https://jsonplaceholder.typicode.com/todos/1') * .then(response => response.json()) * }); - * + * * @example * const result = await retry(() => { * return fetch('https://jsonplaceholder.typicode.com/todos/1') * .then(response => response.json()) * }, { times: 3, delay: 1000 }); - * + * * @since 0.0.8 */ export async function retry( @@ -50,24 +55,25 @@ export async function retry( shouldRetry, } = options; + const wrappedFn = tryIt(fn); + const delayFn = isFunction(delay) ? delay : null; + const delayMs = delayFn ? 0 : delay as number; + + const stop = (error?: any): never => { + throw new RetryEarlyExitError(error); + }; + + let lastError: Error | null = null; let count = 1; - let lastError: Error = new Error('Retry failed'); while (count <= times) { - const metadata = { - count, - stop: (error?: any) => { - throw { [RetryEarlyExit]: error }; - }, - }; - - const { error, data } = await tryIt(fn)(metadata); + const { error, data } = await wrappedFn({ count, stop }); if (!error) return data; - if (RetryEarlyExit in error) - throw error[RetryEarlyExit]; + if (error instanceof RetryEarlyExitError) + throw error.cause; if (shouldRetry && !shouldRetry(error, count)) throw error; @@ -77,12 +83,12 @@ export async function retry( // Don't delay after the last attempt if (count <= times) { - const delayMs = isFunction(delay) ? delay(count) : delay; - - if (delayMs > 0) - await sleep(delayMs); + const ms = delayFn ? delayFn(count) : delayMs; + + if (ms > 0) + await sleep(ms); } } - throw lastError; + throw lastError!; } From 433ab1c2cd32575a034696a913985d8b6916a5de Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 26 Mar 2026 06:15:51 +0700 Subject: [PATCH 6/8] refactor(core/stdlib): streamline tryIt function with improved promise handling --- core/stdlib/src/async/tryIt/index.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/stdlib/src/async/tryIt/index.ts b/core/stdlib/src/async/tryIt/index.ts index e43ab21..bec5838 100644 --- a/core/stdlib/src/async/tryIt/index.ts +++ b/core/stdlib/src/async/tryIt/index.ts @@ -4,21 +4,24 @@ export type TryItReturn = Return extends Promise ? Promise<{ error: Error; data: undefined } | { error: undefined; data: Awaited }> : { error: Error; data: undefined } | { error: undefined; data: Return }; +function onResolve(data: any) { return { error: undefined, data }; } +function onReject(error: any) { return { error, data: undefined }; } + /** * @name tryIt * @category Async * @description Wraps promise-based code in a try/catch block without forking the control flow - * + * * @param {Function} fn - The function to try * @returns {Function} - The function that will return a tuple with the error and the result - * + * * @example * const wrappedFetch = tryIt(fetch); * const { error, data } = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1'); - * + * * @example * const { error, data } = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1'); - * + * * @since 0.0.3 */ export function tryIt( @@ -29,9 +32,7 @@ export function tryIt( const result = fn(...args); if (isPromise(result)) - return result - .then((value) => ({ error: undefined, data: value })) - .catch((error) => ({ error, data: undefined })) as TryItReturn; + return result.then(onResolve).catch(onReject) as TryItReturn; return { error: undefined, data: result } as TryItReturn; } catch (error) { From ed5e6656f1b5f391c355eff6fbd03030f771d80e Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 26 Mar 2026 06:23:48 +0700 Subject: [PATCH 7/8] fix(retry): suppress eslint warning for throwing literal error --- core/stdlib/src/async/retry/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/stdlib/src/async/retry/index.ts b/core/stdlib/src/async/retry/index.ts index 1ac6ccb..8901d98 100644 --- a/core/stdlib/src/async/retry/index.ts +++ b/core/stdlib/src/async/retry/index.ts @@ -90,5 +90,6 @@ export async function retry( } } + // eslint-disable-next-line eslint/no-throw-literal throw lastError!; } From 6b4ddc97336d0ac2e35765d3ecf343403830df1a Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 26 Mar 2026 06:28:09 +0700 Subject: [PATCH 8/8] refactor(retry): simplify error handling in retry tests and ensure consistent promise rejection handling --- core/stdlib/src/async/retry/index.test.ts | 33 ++++++++++++----------- core/stdlib/src/async/tryIt/index.ts | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/core/stdlib/src/async/retry/index.test.ts b/core/stdlib/src/async/retry/index.test.ts index 3fb7131..4fc61e1 100644 --- a/core/stdlib/src/async/retry/index.test.ts +++ b/core/stdlib/src/async/retry/index.test.ts @@ -99,53 +99,56 @@ describe('retry', () => { const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); const retryPromise = retry(failingFn, { times: 3, delay: 1000 }); - + const result = expect(retryPromise).rejects.toThrow('Test error'); + // First call should happen immediately expect(failingFn).toHaveBeenCalledTimes(1); - + // Advance time to trigger first retry await vi.advanceTimersByTimeAsync(1000); expect(failingFn).toHaveBeenCalledTimes(2); - + // Advance time to trigger second retry await vi.advanceTimersByTimeAsync(1000); expect(failingFn).toHaveBeenCalledTimes(3); - - await expect(retryPromise).rejects.toThrow('Test error'); + + await result; }); it('use dynamic delay function', async () => { const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); const delayFn = vi.fn((count: number) => count * 500); - + const retryPromise = retry(failingFn, { times: 3, delay: delayFn }); - + const result = expect(retryPromise).rejects.toThrow('Test error'); + // First call should happen immediately expect(failingFn).toHaveBeenCalledTimes(1); - + // First retry should wait for delay(2) = 1000ms await vi.advanceTimersByTimeAsync(1000); expect(failingFn).toHaveBeenCalledTimes(2); expect(delayFn).toHaveBeenCalledWith(2); - + // Second retry should wait for delay(3) = 1500ms await vi.advanceTimersByTimeAsync(1500); expect(failingFn).toHaveBeenCalledTimes(3); expect(delayFn).toHaveBeenCalledWith(3); - - await expect(retryPromise).rejects.toThrow('Test error'); + + await result; }); it('not delay after the last attempt', async () => { const failingFn = vi.fn().mockRejectedValue(new Error('Test error')); - + const retryPromise = retry(failingFn, { times: 2, delay: 1000 }); - + const result = expect(retryPromise).rejects.toThrow('Test error'); + // Wait for the first retry delay await vi.advanceTimersByTimeAsync(1000); - + // Should complete without further delays - await expect(retryPromise).rejects.toThrow('Test error'); + await result; expect(failingFn).toHaveBeenCalledTimes(2); }); diff --git a/core/stdlib/src/async/tryIt/index.ts b/core/stdlib/src/async/tryIt/index.ts index bec5838..f0734f4 100644 --- a/core/stdlib/src/async/tryIt/index.ts +++ b/core/stdlib/src/async/tryIt/index.ts @@ -32,7 +32,7 @@ export function tryIt( const result = fn(...args); if (isPromise(result)) - return result.then(onResolve).catch(onReject) as TryItReturn; + return result.then(onResolve, onReject) as TryItReturn; return { error: undefined, data: result } as TryItReturn; } catch (error) {