@rafaelsilvadeveloper/resilient-fetch 1.0.2
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 +85 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/resilientFetch.d.ts +6 -0
- package/dist/resilientFetch.js +109 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +2 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @rafaelsilvadeveloper/resilient-fetch
|
|
2
|
+
|
|
3
|
+
A strongly typed, zero-dependency TypeScript wrapper adding retries, exponential backoff, jitter, and circuit breaker patterns to native fetch.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@rafaelsilvadeveloper/resilient-fetch)
|
|
6
|
+
[](https://discord.gg/7Fw7snafYS)
|
|
7
|
+
[](https://www.npmjs.com/package/@rafaelsilvadeveloper/resilient-fetch)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* 🛡️ **TypeScript Definitions**: Matches standard `fetch` signatures out-of-the-box.
|
|
12
|
+
* 📦 **Zero Dependencies**: Built on native `fetch` API. Ideal for Cloudflare Workers, Edge runtimes, Bun, and Node.js.
|
|
13
|
+
* 🚦 **Smart Retries & Backoff**: Exponential backoff with randomized jitter to prevent server overload.
|
|
14
|
+
* 🔌 **Circuit Breaker**: Auto-trip requests going to failing host domains to protect application resources.
|
|
15
|
+
* ⏱️ **Timeout Handling**: Built-in request timeouts utilizing native AbortControllers.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @rafaelsilvadeveloper/resilient-fetch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
Replace your native `fetch` with `resilientFetch` for automatic protection:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { resilientFetch } from '@rafaelsilvadeveloper/resilient-fetch';
|
|
29
|
+
|
|
30
|
+
async function run() {
|
|
31
|
+
// Works exactly like standard fetch, but retries under 429, 500, 502, 503, 504 errors!
|
|
32
|
+
const response = await resilientFetch('https://api.example.com/users');
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
console.log(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
run();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Custom Resilience Options
|
|
41
|
+
|
|
42
|
+
Create a custom configured resilient fetch instance:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createResilientFetch } from '@rafaelsilvadeveloper/resilient-fetch';
|
|
46
|
+
|
|
47
|
+
const customFetch = createResilientFetch({
|
|
48
|
+
retries: 4, // Retry up to 4 times
|
|
49
|
+
initialDelayMs: 250, // Start backing off at 250ms
|
|
50
|
+
maxDelayMs: 5000, // Backoff capped at 5 seconds
|
|
51
|
+
timeoutMs: 3000, // Abort request if it takes longer than 3 seconds
|
|
52
|
+
retryOnStatus: [429, 503], // Only retry on Rate Limit and Service Unavailable
|
|
53
|
+
circuitBreaker: {
|
|
54
|
+
failureThreshold: 5, // Trip breaker after 5 failures to the same host
|
|
55
|
+
cooldownMs: 30000 // Cooldown host for 30 seconds before retrying
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
async function run() {
|
|
60
|
+
const response = await customFetch('https://api.my-service.com/data');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
run();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Error Handling
|
|
67
|
+
|
|
68
|
+
Throws standard fetch errors or AbortErrors on timeout.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
try {
|
|
72
|
+
await resilientFetch('https://failing-api.com');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Request failed after all retries:', error);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Support
|
|
79
|
+
|
|
80
|
+
For support, questions, or discussions, join our Discord server:
|
|
81
|
+
|
|
82
|
+
[](https://discord.gg/7Fw7snafYS)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resilientFetch = exports.createResilientFetch = void 0;
|
|
4
|
+
var resilientFetch_1 = require("./resilientFetch");
|
|
5
|
+
Object.defineProperty(exports, "createResilientFetch", { enumerable: true, get: function () { return resilientFetch_1.createResilientFetch; } });
|
|
6
|
+
Object.defineProperty(exports, "resilientFetch", { enumerable: true, get: function () { return resilientFetch_1.resilientFetch; } });
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resilientFetch = void 0;
|
|
4
|
+
exports.createResilientFetch = createResilientFetch;
|
|
5
|
+
class CircuitBreaker {
|
|
6
|
+
constructor(threshold = 5, cooldown = 10000) {
|
|
7
|
+
this.failures = 0;
|
|
8
|
+
this.state = 'CLOSED';
|
|
9
|
+
this.nextAttemptTime = 0;
|
|
10
|
+
this.threshold = threshold;
|
|
11
|
+
this.cooldown = cooldown;
|
|
12
|
+
}
|
|
13
|
+
checkCall() {
|
|
14
|
+
if (this.state === 'OPEN') {
|
|
15
|
+
if (Date.now() >= this.nextAttemptTime) {
|
|
16
|
+
this.state = 'HALF_OPEN';
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
recordSuccess() {
|
|
24
|
+
this.failures = 0;
|
|
25
|
+
this.state = 'CLOSED';
|
|
26
|
+
}
|
|
27
|
+
recordFailure() {
|
|
28
|
+
this.failures++;
|
|
29
|
+
if (this.failures >= this.threshold) {
|
|
30
|
+
this.state = 'OPEN';
|
|
31
|
+
this.nextAttemptTime = Date.now() + this.cooldown;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const breakers = new Map();
|
|
36
|
+
function getBreaker(key, threshold, cooldown) {
|
|
37
|
+
let breaker = breakers.get(key);
|
|
38
|
+
if (!breaker) {
|
|
39
|
+
breaker = new CircuitBreaker(threshold, cooldown);
|
|
40
|
+
breakers.set(key, breaker);
|
|
41
|
+
}
|
|
42
|
+
return breaker;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a fetch client wrapped in resilience logic.
|
|
46
|
+
*/
|
|
47
|
+
function createResilientFetch(globalOptions = {}) {
|
|
48
|
+
return async (input, init) => {
|
|
49
|
+
const urlString = input instanceof URL ? input.toString() : typeof input === 'string' ? input : input.url;
|
|
50
|
+
let host = 'global';
|
|
51
|
+
try {
|
|
52
|
+
host = new URL(urlString).host;
|
|
53
|
+
}
|
|
54
|
+
catch (_) {
|
|
55
|
+
// Ignored
|
|
56
|
+
}
|
|
57
|
+
const retries = globalOptions.retries ?? 3;
|
|
58
|
+
const initialDelay = globalOptions.initialDelayMs ?? 500;
|
|
59
|
+
const maxDelay = globalOptions.maxDelayMs ?? 10000;
|
|
60
|
+
const backoffFactor = globalOptions.backoffFactor ?? 2;
|
|
61
|
+
const retryOnStatus = globalOptions.retryOnStatus ?? [429, 500, 502, 503, 504];
|
|
62
|
+
const timeout = globalOptions.timeoutMs;
|
|
63
|
+
const cbOptions = globalOptions.circuitBreaker || {};
|
|
64
|
+
const breaker = getBreaker(host, cbOptions.failureThreshold, cbOptions.cooldownMs);
|
|
65
|
+
if (!breaker.checkCall()) {
|
|
66
|
+
throw new Error(`Circuit Breaker is OPEN for host ${host}. Request blocked.`);
|
|
67
|
+
}
|
|
68
|
+
let attempt = 0;
|
|
69
|
+
const executeWithTimeout = async (requestInit) => {
|
|
70
|
+
if (!timeout) {
|
|
71
|
+
return fetch(input, requestInit);
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const signal = controller.signal;
|
|
75
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(input, { ...requestInit, signal });
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
while (true) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await executeWithTimeout(init || {});
|
|
89
|
+
if (retryOnStatus.includes(response.status) && attempt < retries) {
|
|
90
|
+
throw new Error(`Status ${response.status} triggered retry.`);
|
|
91
|
+
}
|
|
92
|
+
breaker.recordSuccess();
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
attempt++;
|
|
97
|
+
breaker.recordFailure();
|
|
98
|
+
if (attempt > retries || err.name === 'AbortError') {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
const delay = Math.min(maxDelay, initialDelay * Math.pow(backoffFactor, attempt - 1));
|
|
102
|
+
const jitter = Math.random() * 0.3 * delay; // 30% randomized jitter
|
|
103
|
+
const finalDelay = delay + jitter;
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, finalDelay));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
exports.resilientFetch = createResilientFetch();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ResilientOptions {
|
|
2
|
+
retries?: number;
|
|
3
|
+
initialDelayMs?: number;
|
|
4
|
+
maxDelayMs?: number;
|
|
5
|
+
backoffFactor?: number;
|
|
6
|
+
retryOnStatus?: number[];
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
circuitBreaker?: {
|
|
9
|
+
failureThreshold?: number;
|
|
10
|
+
cooldownMs?: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rafaelsilvadeveloper/resilient-fetch",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A strongly typed, zero-dependency TypeScript wrapper adding retries, exponential backoff, jitter, and circuit breaker patterns to native fetch.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/rafael-packages/resilient-fetch.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/rafael-packages/resilient-fetch/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rafael-packages/resilient-fetch#readme",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"prepare": "bun run build",
|
|
23
|
+
"lint": "eslint src/**/*.ts",
|
|
24
|
+
"format": "prettier --write src/**/*.ts tests/**/*.ts"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"fetch",
|
|
28
|
+
"retry",
|
|
29
|
+
"backoff",
|
|
30
|
+
"resilience",
|
|
31
|
+
"circuit-breaker",
|
|
32
|
+
"jitter",
|
|
33
|
+
"typescript"
|
|
34
|
+
],
|
|
35
|
+
"author": "realkalashnikov",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.9.1",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
43
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
44
|
+
"eslint": "^8.57.0",
|
|
45
|
+
"prettier": "^3.3.3"
|
|
46
|
+
}
|
|
47
|
+
}
|