Skip to content

Commit 63d1823

Browse files
committed
feat: implement retry helper
1 parent 540d020 commit 63d1823

File tree

6 files changed

+134
-10
lines changed

6 files changed

+134
-10
lines changed

package-lock.json

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/common",
3-
"version": "5.10.0",
3+
"version": "5.11.0",
44
"description": "The Athenna common helpers to use in any Node.js ESM project.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",
@@ -75,6 +75,7 @@
7575
},
7676
"dependencies": {
7777
"@fastify/formbody": "^8.0.2",
78+
"@humanwhocodes/retry": "^0.4.3",
7879
"bytes": "^3.1.2",
7980
"callsite": "^1.0.0",
8081
"chalk": "^5.4.1",

src/helpers/Enum.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ export class Enum extends Macroable {
1616
* @example
1717
* ```ts
1818
* export class StatusEnum extends Enum {
19-
* static PENDING = 'pending'
20-
* static APPROVED = 'approved'
21-
* static BLOCKED = 'blocked'
19+
* public static PENDING = 'pending' as const
20+
* public static APPROVED = 'approved' as const
21+
* public static BLOCKED = 'blocked' as const
2222
* }
2323
*
2424
* const keys = StatusEnum.keys() // [ 'PENDING', 'APPROVED', 'BLOCKED' ]
@@ -34,9 +34,9 @@ export class Enum extends Macroable {
3434
* @example
3535
* ```ts
3636
* export class StatusEnum extends Enum {
37-
* static PENDING = 'pending'
38-
* static APPROVED = 'approved'
39-
* static BLOCKED = 'blocked'
37+
* public static PENDING = 'pending' as const
38+
* public static static APPROVED = 'approved' as const
39+
* public static BLOCKED = 'blocked' as const
4040
* }
4141
*
4242
* const values = StatusEnum.values() // ['pending', 'approved', 'blocked']
@@ -52,7 +52,7 @@ export class Enum extends Macroable {
5252
* @example
5353
* ```ts
5454
* export class StatusEnum extends Enum {
55-
* static PENDING = 'pending'
55+
* public static PENDING = 'pending' as const
5656
* }
5757
*
5858
* const entries = StatusEnum.entries() // [['PENDING', 'pending']]

src/helpers/Retry.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @athenna/common
3+
*
4+
* (c) João Lenon <lenon@athenna.io>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { Retrier } from '@humanwhocodes/retry'
11+
import type { Exception } from '#src/helpers/Exception'
12+
13+
export class RetryBuilder {
14+
private fn: (error: Error | Exception) => Promise<any>
15+
private options: {
16+
timeout?: number
17+
maxDelay?: number
18+
concurrency?: number
19+
} = {}
20+
21+
/**
22+
* Set the timeout for the retry.
23+
*/
24+
public setTimeout(timeout: number) {
25+
this.options.timeout = timeout
26+
27+
return this
28+
}
29+
30+
/**
31+
* Set the maximum delay between retries.
32+
*/
33+
public setMaxDelay(maxDelay: number) {
34+
this.options.maxDelay = maxDelay
35+
36+
return this
37+
}
38+
39+
/**
40+
* Set the number of concurrent retries.
41+
*/
42+
public setConcurrency(concurrency: number) {
43+
this.options.concurrency = concurrency
44+
45+
return this
46+
}
47+
48+
/**
49+
* Create a new retry instance to retry a function when it
50+
* throws an error. Firstly define which errors should be
51+
* retried and then define the function to retry.
52+
*
53+
* @example
54+
* ```ts
55+
* const text = await Retry
56+
* .build()
57+
* .whenError(error => error.code === 'ENFILE')
58+
* .retry(() => fs.readFile('README.md', 'utf8'))
59+
* ```
60+
*/
61+
public whenError(fn: (error: Exception) => any | Promise<any>) {
62+
this.fn = fn
63+
64+
return new Retrier(this.fn, this.options)
65+
}
66+
}
67+
68+
export class Retry {
69+
/**
70+
* Create a new retry builder instance.
71+
*/
72+
public static build() {
73+
return new RetryBuilder()
74+
}
75+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export * from '#src/helpers/Options'
3131
export * from '#src/helpers/Otp'
3232
export * from '#src/helpers/Parser'
3333
export * from '#src/helpers/Path'
34+
export * from '#src/helpers/Retry'
3435
export * from '#src/helpers/Route'
3536
export * from '#src/helpers/Sleep'
3637
export * from '#src/helpers/String'

tests/unit/helpers/RetryTest.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @athenna/common
3+
*
4+
* (c) João Lenon <lenon@athenna.io>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { Retry } from '#src'
11+
import { readFile } from 'fs/promises'
12+
import { Test, type Context } from '@athenna/test'
13+
14+
export default class RetryTest {
15+
@Test()
16+
public async shouldRetryAFunctionWhenItThrowsASpecificError({ assert }: Context) {
17+
await assert.rejects(() =>
18+
Retry.build()
19+
.setTimeout(500)
20+
.whenError(error => error.code === 'ENOENT')
21+
.retry(() => readFile('READMEE.md', 'utf8'))
22+
)
23+
}
24+
25+
@Test()
26+
public async shouldRetryAFunctionWhenItThrowsASpecificErrorAndReturnTheResult({ assert }: Context) {
27+
const text = await Retry.build()
28+
.whenError(error => error.code === 'ENOENT')
29+
.retry(() => readFile('README.md', 'utf8'))
30+
31+
assert.isDefined(text)
32+
}
33+
}

0 commit comments

Comments
 (0)