@jobrain/retry-x 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +103 -0
- package/dist/index.mjs +76 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# retry-x
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
A tiny, framework-agnostic retry library that wraps any async function with smart backoff, jitter, and full control — done right.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Exponential backoff**: Wait grows after each failure.
|
|
11
|
+
- **Jitter**: Adds randomness to backoff to prevent thundering herd.
|
|
12
|
+
- **Max attempts**: Stop after N retries and throw the last error.
|
|
13
|
+
- **Max timeout**: Stop retrying after a total elapsed time.
|
|
14
|
+
- **`retryIf` condition**: Only retry when a custom function returns `true`.
|
|
15
|
+
- **`onRetry` callback**: Hook into each retry attempt for logging / metrics.
|
|
16
|
+
- **TypeScript types**: Full type safety out of the box.
|
|
17
|
+
- **Zero dependencies**: Extremely lightweight.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install retry-x
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Minimal — works out of the box
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { retry } from "retry-x";
|
|
31
|
+
|
|
32
|
+
const data = await retry(() => fetch("https://api.example.com/data"));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Full control
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { retry } from "retry-x";
|
|
39
|
+
|
|
40
|
+
const data = await retry(
|
|
41
|
+
() => fetch("https://api.example.com/data"),
|
|
42
|
+
{
|
|
43
|
+
attempts: 5,
|
|
44
|
+
backoff: "exponential", // or "linear" or "fixed"
|
|
45
|
+
jitter: true,
|
|
46
|
+
timeout: 10_000, // give up after 10 seconds total
|
|
47
|
+
retryIf: (err, attempt) => err.status === 503,
|
|
48
|
+
onRetry: (err, attempt) => {
|
|
49
|
+
console.log(`Retry #${attempt}:`, err.message);
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If all attempts fail, the last error is thrown — so your existing `try/catch` just works.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `retry(fn, options?)`
|
|
60
|
+
|
|
61
|
+
Wraps an async function and retries it based on the provided options. Returns the result of `fn` on success, or throws the last error after all attempts are exhausted.
|
|
62
|
+
|
|
63
|
+
#### Options
|
|
64
|
+
|
|
65
|
+
| Option | Type | Default | Description |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `attempts` | `number` | `3` | Maximum number of attempts |
|
|
68
|
+
| `backoff` | `'exponential' \| 'linear' \| 'fixed'` | `'exponential'` | Backoff strategy between retries |
|
|
69
|
+
| `initialDelay` | `number` | `1000` | Initial delay in milliseconds |
|
|
70
|
+
| `maxDelay` | `number` | `30000` | Maximum delay in milliseconds |
|
|
71
|
+
| `jitter` | `boolean` | `true` | Adds randomness to delay to avoid thundering herd |
|
|
72
|
+
| `timeout` | `number` | — | Total timeout across all attempts in milliseconds |
|
|
73
|
+
| `retryIf` | `(error, attempt) => boolean` | — | Return `false` to stop retrying early |
|
|
74
|
+
| `onRetry` | `(error, attempt) => void` | — | Called before each retry attempt |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type RetryBackoff = 'fixed' | 'linear' | 'exponential';
|
|
2
|
+
interface RetryOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of attempts.
|
|
5
|
+
* @default 3
|
|
6
|
+
*/
|
|
7
|
+
attempts?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Backoff strategy to use.
|
|
10
|
+
* @default 'exponential'
|
|
11
|
+
*/
|
|
12
|
+
backoff?: RetryBackoff;
|
|
13
|
+
/**
|
|
14
|
+
* Initial delay in milliseconds.
|
|
15
|
+
* @default 1000
|
|
16
|
+
*/
|
|
17
|
+
initialDelay?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Maximum delay in milliseconds.
|
|
20
|
+
* @default 30000
|
|
21
|
+
*/
|
|
22
|
+
maxDelay?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to add jitter to the delay.
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
jitter?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Total timeout for all attempts in milliseconds.
|
|
30
|
+
* If exceeded, the last error will be thrown.
|
|
31
|
+
*/
|
|
32
|
+
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Predicate function to determine if a retry should be attempted.
|
|
35
|
+
* If it returns false, retrying stops immediately.
|
|
36
|
+
*/
|
|
37
|
+
retryIf?: (error: any, attempt: number) => boolean | Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Callback function called before each retry attempt.
|
|
40
|
+
*/
|
|
41
|
+
onRetry?: (error: any, attempt: number) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare function retry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>;
|
|
45
|
+
|
|
46
|
+
export { type RetryBackoff, type RetryOptions, retry };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type RetryBackoff = 'fixed' | 'linear' | 'exponential';
|
|
2
|
+
interface RetryOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of attempts.
|
|
5
|
+
* @default 3
|
|
6
|
+
*/
|
|
7
|
+
attempts?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Backoff strategy to use.
|
|
10
|
+
* @default 'exponential'
|
|
11
|
+
*/
|
|
12
|
+
backoff?: RetryBackoff;
|
|
13
|
+
/**
|
|
14
|
+
* Initial delay in milliseconds.
|
|
15
|
+
* @default 1000
|
|
16
|
+
*/
|
|
17
|
+
initialDelay?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Maximum delay in milliseconds.
|
|
20
|
+
* @default 30000
|
|
21
|
+
*/
|
|
22
|
+
maxDelay?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to add jitter to the delay.
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
jitter?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Total timeout for all attempts in milliseconds.
|
|
30
|
+
* If exceeded, the last error will be thrown.
|
|
31
|
+
*/
|
|
32
|
+
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Predicate function to determine if a retry should be attempted.
|
|
35
|
+
* If it returns false, retrying stops immediately.
|
|
36
|
+
*/
|
|
37
|
+
retryIf?: (error: any, attempt: number) => boolean | Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Callback function called before each retry attempt.
|
|
40
|
+
*/
|
|
41
|
+
onRetry?: (error: any, attempt: number) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare function retry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>;
|
|
45
|
+
|
|
46
|
+
export { type RetryBackoff, type RetryOptions, retry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
retry: () => retry
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/backoff.ts
|
|
28
|
+
function calculateDelay(strategy, attempt, initialDelay, maxDelay, jitter) {
|
|
29
|
+
let delay = 0;
|
|
30
|
+
switch (strategy) {
|
|
31
|
+
case "fixed":
|
|
32
|
+
delay = initialDelay;
|
|
33
|
+
break;
|
|
34
|
+
case "linear":
|
|
35
|
+
delay = initialDelay * attempt;
|
|
36
|
+
break;
|
|
37
|
+
case "exponential":
|
|
38
|
+
delay = initialDelay * Math.pow(2, attempt - 1);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
delay = Math.min(delay, maxDelay);
|
|
42
|
+
if (jitter) {
|
|
43
|
+
delay = Math.random() * delay;
|
|
44
|
+
}
|
|
45
|
+
return delay;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/retry.ts
|
|
49
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
async function retry(fn, options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
attempts = 3,
|
|
53
|
+
backoff = "exponential",
|
|
54
|
+
initialDelay = 1e3,
|
|
55
|
+
maxDelay = 3e4,
|
|
56
|
+
jitter = true,
|
|
57
|
+
timeout,
|
|
58
|
+
retryIf,
|
|
59
|
+
onRetry
|
|
60
|
+
} = options;
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
let lastError;
|
|
63
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
64
|
+
if (attempt > 1 && timeout && Date.now() - startTime >= timeout) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return await Promise.resolve().then(() => fn());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
lastError = error;
|
|
71
|
+
if (attempt >= attempts) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (retryIf && !await retryIf(error, attempt)) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (onRetry) {
|
|
78
|
+
await onRetry(error, attempt);
|
|
79
|
+
}
|
|
80
|
+
const delay = calculateDelay(
|
|
81
|
+
backoff,
|
|
82
|
+
attempt,
|
|
83
|
+
initialDelay,
|
|
84
|
+
maxDelay,
|
|
85
|
+
jitter
|
|
86
|
+
);
|
|
87
|
+
if (timeout) {
|
|
88
|
+
const remainingTime = timeout - (Date.now() - startTime);
|
|
89
|
+
if (remainingTime <= 0) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
await sleep(Math.min(delay, remainingTime));
|
|
93
|
+
} else {
|
|
94
|
+
await sleep(delay);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw lastError;
|
|
99
|
+
}
|
|
100
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
101
|
+
0 && (module.exports = {
|
|
102
|
+
retry
|
|
103
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/backoff.ts
|
|
2
|
+
function calculateDelay(strategy, attempt, initialDelay, maxDelay, jitter) {
|
|
3
|
+
let delay = 0;
|
|
4
|
+
switch (strategy) {
|
|
5
|
+
case "fixed":
|
|
6
|
+
delay = initialDelay;
|
|
7
|
+
break;
|
|
8
|
+
case "linear":
|
|
9
|
+
delay = initialDelay * attempt;
|
|
10
|
+
break;
|
|
11
|
+
case "exponential":
|
|
12
|
+
delay = initialDelay * Math.pow(2, attempt - 1);
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
delay = Math.min(delay, maxDelay);
|
|
16
|
+
if (jitter) {
|
|
17
|
+
delay = Math.random() * delay;
|
|
18
|
+
}
|
|
19
|
+
return delay;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/retry.ts
|
|
23
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
async function retry(fn, options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
attempts = 3,
|
|
27
|
+
backoff = "exponential",
|
|
28
|
+
initialDelay = 1e3,
|
|
29
|
+
maxDelay = 3e4,
|
|
30
|
+
jitter = true,
|
|
31
|
+
timeout,
|
|
32
|
+
retryIf,
|
|
33
|
+
onRetry
|
|
34
|
+
} = options;
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
let lastError;
|
|
37
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
38
|
+
if (attempt > 1 && timeout && Date.now() - startTime >= timeout) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.resolve().then(() => fn());
|
|
43
|
+
} catch (error) {
|
|
44
|
+
lastError = error;
|
|
45
|
+
if (attempt >= attempts) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (retryIf && !await retryIf(error, attempt)) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (onRetry) {
|
|
52
|
+
await onRetry(error, attempt);
|
|
53
|
+
}
|
|
54
|
+
const delay = calculateDelay(
|
|
55
|
+
backoff,
|
|
56
|
+
attempt,
|
|
57
|
+
initialDelay,
|
|
58
|
+
maxDelay,
|
|
59
|
+
jitter
|
|
60
|
+
);
|
|
61
|
+
if (timeout) {
|
|
62
|
+
const remainingTime = timeout - (Date.now() - startTime);
|
|
63
|
+
if (remainingTime <= 0) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
await sleep(Math.min(delay, remainingTime));
|
|
67
|
+
} else {
|
|
68
|
+
await sleep(delay);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw lastError;
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
retry
|
|
76
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jobrain/retry-x",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny, framework-agnostic retry library with smart backoff and jitter.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
13
|
+
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"lint": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"retry",
|
|
20
|
+
"backoff",
|
|
21
|
+
"exponential",
|
|
22
|
+
"jitter",
|
|
23
|
+
"async",
|
|
24
|
+
"promise"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.5.2",
|
|
30
|
+
"tsup": "^8.0.2",
|
|
31
|
+
"typescript": "^5.4.3",
|
|
32
|
+
"vitest": "^1.4.0"
|
|
33
|
+
}
|
|
34
|
+
}
|