@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 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
+ [![NPM Version](https://img.shields.io/npm/v/@rafaelsilvadeveloper/resilient-fetch.svg?style=flat-square)](https://www.npmjs.com/package/@rafaelsilvadeveloper/resilient-fetch)
6
+ [![Discord Support](https://img.shields.io/discord/1111111111?color=7289da&label=Discord&logo=discord&style=flat-square)](https://discord.gg/7Fw7snafYS)
7
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-blueviolet.svg?style=flat-square)](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
+ [![Discord Server](https://img.shields.io/discord/1111111111?color=7289da&label=Discord&logo=discord&style=for-the-badge)](https://discord.gg/7Fw7snafYS)
83
+
84
+ ## License
85
+ MIT
@@ -0,0 +1,2 @@
1
+ export { createResilientFetch, resilientFetch } from './resilientFetch';
2
+ export type * from './types';
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,6 @@
1
+ import type { ResilientOptions } from './types';
2
+ /**
3
+ * Creates a fetch client wrapped in resilience logic.
4
+ */
5
+ export declare function createResilientFetch(globalOptions?: ResilientOptions): typeof fetch;
6
+ export declare const resilientFetch: typeof fetch;
@@ -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();
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }