@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 ADDED
@@ -0,0 +1,78 @@
1
+ # retry-x
2
+
3
+ ![npm](https://img.shields.io/npm/v/retry-x)
4
+ ![license](https://img.shields.io/npm/l/retry-x)
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
@@ -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 };
@@ -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
+ }