Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(async module): added onRetry option for retry fn #229

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/async/retry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The `times` option defaults to `3`. The `delay` option (defaults to null) can sp

The `backoff` option is like delay but uses a function to sleep -- makes for easy exponential backoff.

The `onRetry` option is a Function that is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter and the attempt number.

```ts
import * as _ from 'radashi'

Expand All @@ -21,4 +23,10 @@ await _.retry({ times: 2, delay: 1000 }, api.users.list)

// exponential backoff
await _.retry({ backoff: i => 10 ** i }, api.users.list)

// onRetry usage
await _.retry(
{ onRetry: (err, i) => console.log(`Trying again... Attempt: ${i}`) },
api.users.list,
)
```
14 changes: 13 additions & 1 deletion src/async/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ import { sleep, tryit } from 'radashi'
export type RetryOptions = {
times?: number
delay?: number | null
onRetry?: (err: Error, attemptNumber: number) => void
backoff?: (count: number) => number
}

/**
* Retries the given function the specified number of times.
*
* @param {Object} options - The employee who is responsible for the project.
* @param {number} [options.times=3] - Number of attempts.
* @param {number} [options.delay] - Specify milliseconds to sleep between attempts.
* @param {Function} [options.onRetry=null] - Is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter and the attempt number.
* @param {Function} [options.backoff=null] - The backoff option is like delay but uses a function to sleep -- makes for easy exponential backoff.
* @param {Function} func - Function to be executed
*
* @see https://radashi.js.org/reference/async/retry
* @example
* ```ts
Comment on lines 3 to 22
Copy link
Contributor

@MarlonPassos-git MarlonPassos-git Sep 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I said to use Jsoc, I meant to use it in the type, because using it only in the function would not have a good integration with VSCode.

after:
image

before:
image

Suggested change
export type RetryOptions = {
times?: number
delay?: number | null
onRetry?: (err: Error, attemptNumber: number) => void
backoff?: (count: number) => number
}
/**
* Retries the given function the specified number of times.
*
* @param {Object} options - The employee who is responsible for the project.
* @param {number} [options.times=3] - Number of attempts.
* @param {number} [options.delay] - Specify milliseconds to sleep between attempts.
* @param {Function} [options.onRetry=null] - Is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter and the attempt number.
* @param {Function} [options.backoff=null] - The backoff option is like delay but uses a function to sleep -- makes for easy exponential backoff.
* @param {Function} func - Function to be executed
*
* @see https://radashi.js.org/reference/async/retry
* @example
* ```ts
export type RetryOptions = {
/**
* Number of attempts.
*
* @default 3
*/
times?: number
/** Specify milliseconds to sleep between attempts */
delay?: number | null
/**
* Is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter and the attempt number.
*
* @default null
*/
onRetry?: (err: Error, attemptNumber: number) => void
/**
* The backoff option is like delay but uses a function to sleep -- makes for easy exponential backoff.
*
* @default null
*/
backoff?: (count: number) => number
}
/**
* Retries the given function the specified number of times.
*
* @see https://radashi.js.org/reference/async/retry
* @example
* ```ts

* const result = await retry({ times: 3, delay: 1000 }, async () => {
* const result = await retry({ times: 3, delay: 1000, onRetry: (err, i) => console.log(`Trying again... Attempt: ${i}`) } }, async () => {
* return await fetch('https://example.com')
* })
* ```
Expand All @@ -25,6 +33,7 @@ export async function retry<TResponse>(
const times = options?.times ?? 3
const delay = options?.delay
const backoff = options?.backoff ?? null
const onRetry = options?.onRetry ?? null
let i = 0
while (true) {
const [err, result] = (await tryit(func)((err: any) => {
Expand All @@ -39,6 +48,9 @@ export async function retry<TResponse>(
if (++i >= times) {
throw err
}
if (onRetry) {
onRetry(err, i)
}
if (delay) {
await sleep(delay)
}
Expand Down
21 changes: 19 additions & 2 deletions tests/async/retry.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// cSpell:ignore backoffs

import * as _ from 'radashi'
import type { RetryOptions } from 'radashi'
import * as _ from 'radashi'

const cast = <T = RetryOptions>(value: any): T => value

Expand All @@ -16,9 +16,11 @@ describe('retry', () => {
expect(result).toBe('hello')
})
test('simple + quick + happy path', async () => {
const result = await _.retry(cast(null), async () => {
const onRetry = vi.fn()
const result = await _.retry({ onRetry }, async () => {
return 'hello'
})
expect(onRetry).not.toBeCalled()
expect(result).toBe('hello')
})
test('retries on failure', async () => {
Expand All @@ -32,6 +34,21 @@ describe('retry', () => {
})
expect(result).toBe('hello')
})
test('call onRetry function on retries', async () => {
let attempt = 0
const onRetry = vi.fn()
const result = await _.retry({ onRetry }, async _bail => {
if (attempt < 2) {
attempt++
throw 'Failing for test'
}
return 'hello'
})
expect(onRetry).toBeCalledTimes(2)
expect(onRetry).toHaveBeenNthCalledWith(1, 'Failing for test', 1)
expect(onRetry).toHaveBeenNthCalledWith(2, 'Failing for test', 2)
expect(result).toBe('hello')
})
test('quits on bail', async () => {
try {
await _.retry({}, async bail => {
Expand Down
Loading