vigor-fetch 1.0.12 → 1.0.14

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.
Files changed (2) hide show
  1. package/README.md +249 -110
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,150 +1,289 @@
1
- # vigor-fetch 🚀
1
+ # vigor-fetch
2
2
 
3
- **Vigor** is a lightweight, zero-dependency HTTP client designed for resilience. It intelligently handles server rate limits (429 Too Many Requests) and network instability with built-in retry mechanisms.
3
+ **Vigor** is a lightweight TypeScript HTTP utility library.
4
+ It lets you compose `fetch`, retry, response parsing, and parallel requests using a clean fluent chaining API.
4
5
 
5
- ## ✨ Features
6
+ ---
6
7
 
7
- - 🛡️ **Smart Resilience:** Automatically handles 429 errors by respecting `Retry-After` headers.
8
- - 📈 **Exponential Backoff & Jitter:** Prevents server hammering by increasing wait times with random variation.
9
- - ⚡ **Zero Dependencies:** Built using native Fetch API and AbortController.
10
- - ⚡ **Tiny footprint** (~3 KB)
11
- - 🎯 **Fully Type-Safe:** Written in TypeScript for excellent developer experience and auto-completion.
12
- - 🚦 **Concurrency Control**: Execute bulk requests with a global limit and inter-request jitter using the VigorAll engine.
13
- - 🎯 **Immutable Chaining**: Every configuration method returns a new instance, making it perfect for base templates and functional patterns.
8
+ ## Features
14
9
 
15
- ## 📦 Installation
16
- ```powershell
10
+ - 🔁 **Auto Retry** — Exponential backoff with jitter support
11
+ - 🌐 **Smart Fetch** — Automatic 429 Rate Limit header handling, configurable unretry status codes
12
+ - 📦 **Auto Parsing** — Content-Type based response parsing (JSON, Blob, FormData, etc.)
13
+ - ⚡ **Parallel Requests** — `Promise.allSettled` wrapper with concurrency limit and jitter
14
+ - 🔌 **Interceptors** — Lifecycle hooks: `before` / `after` / `onRetry` / `onError`
15
+ - 🧩 **Plugins** — Extend functionality via the `use()` method
16
+ - 💡 **Immutable Chaining** — Every config method returns a new instance with no side effects
17
+ - ⚡ **Zero Dependencies** — Built using native Fetch API and AbortController.
18
+ ---
17
19
 
20
+ ## Why Vigor?
21
+
22
+ | Feature | Vigor | native fetch | ky | axios |
23
+ |---|:---:|:---:|:---:|:---:|
24
+ | Zero dependencies | ✅ | ✅ | ✅ | ❌ |
25
+ | Auto retry with backoff | ✅ | ❌ | ✅ | ❌ |
26
+ | 429 rate-limit header handling | ✅ | ❌ | ❌ | ❌ |
27
+ | Jitter on retry | ✅ | ❌ | ❌ | ❌ |
28
+ | Auto response parsing | ✅ | ❌ | partial | partial |
29
+ | Fluent chaining API | ✅ | ❌ | ✅ | ❌ |
30
+ | Concurrency-limited parallel requests | ✅ | ❌ | ❌ | ❌ |
31
+ | Lifecycle interceptors | ✅ | ❌ | ✅ | ✅ |
32
+ | Plugin system | ✅ | ❌ | ❌ | ❌ |
33
+ | TypeScript-first | ✅ | ❌ | ✅ | partial |
34
+
35
+ ## Installation
36
+
37
+ ```bash
18
38
  npm install vigor-fetch
19
-
20
39
  ```
21
40
 
22
- ## Why Vigor?
41
+ ---
23
42
 
24
- | Feature | Vigor | Axios | Native fetch |
25
- |-------|------|------|-------------|
26
- | Built-in Retry | ✅ | ❌ | ❌ |
27
- | Rate-limit handling | ✅ | ❌ | ❌ |
28
- | Concurrency control | ✅ | ❌ | ❌ |
29
- | Zero dependencies | ✅ | ❌ | ✅ |
30
- | Immutable request builder | ✅ | ❌ | ❌ |
31
-
32
- ## Use Cases
33
-
34
- **Vigor is useful when:**
35
-
36
- - Your API frequently returns 429 (Too Many Requests)
37
- - You need automatic retry with exponential backoff
38
- - You want concurrency control for batch requests
39
- - You prefer immutable request builders
40
-
41
- ## 🛠️ API References
42
-
43
- 1. **vigor.fetch(origin)**
44
-
45
- | Method | Type | Default | Description
46
- | :--- | :--- | :--- | :--- |
47
- | .path(arg) | string | "" | Sets the endpoint path to be appended to the origin. |
48
- | .query(arg) | Record<string, any> | {} | Appends key-value pairs as query parameters to the URL. |
49
- | .method(arg) | string | "POST" | "GET" (depends on body) | Sets the HTTP request method. |
50
- | .headers(arg) | Record<string, string> | {} | Sets the HTTP request headers. |
51
- | .body(arg) | any | null | Sets the request body |
52
- | .offset(arg) | RequestInit | {} | Provides raw fetch options to be merged into the request. |
53
- | .count(arg) | number | 5 | Specifies the maximum number of retry attempts for the request. |
54
- | .max(arg) | number | 5000 | Sets the timeout (in ms) for each individual request attempt. |
55
- | .wait(arg) | number | 10000 | Sets the maximum wait time (in ms) between retry attempts. |
56
- | .backoff(arg) | number | 1.3 | The multiplier used for exponential backoff between retries. |
57
- | .jitter(arg) | number | 500 | Adds a random delay (up to this value in ms) to prevent thundering herd issues. |
58
- | .unretry(arg[]) | number[] | [400, 401, 403, 404, 405, 413, 422] | List of HTTP status codes that should NOT trigger a retry. |
59
- | .retryHeader(...arg) | string[] | ['retry-after', ...] | Custom headers to check for server-defined retry delays |
60
- | .original(bool) | boolean | false | If true, returns the raw Response object instead of parsed data. |
61
- | .parse(key) | keyof Response | null | Forces the use of a specific Response method (e.g., 'json', 'blob') for parsing. |
62
- | .beforeRequest(...hooks) | Function[] | [] | Hooks executed to modify request options before the fetch occurs. |
63
- | .afterRequest(...hooks) | Function[] | [] | Hooks executed immediately after the fetch, before the status check. |
64
- | .beforeResponse(...hooks) | Function[] | [] | Hooks executed on the Response object before parsing the body. |
65
- | .afterResponse(...hooks) | Function[] | [] | Hooks executed on the parsed data before returning the final result. |
66
- | .onError(...hooks) | Function[] | [] | Hooks to handle errors; can recover from error by returning a value. |
67
- | .request() | Promise<T> | - | Executes the request logic including retries and hooks. |
68
-
69
- 2. **Vigor().all(config)**
70
-
71
- | Method | Type | Default | Description
72
- | :--- | :--- | :--- | :--- |
73
- | .limit(arg) | number | 10 | Maximum number of concurrent promises running at once. |
74
- | .jitter(arg) | number | 1000 | Random delay (ms) applied before each task starts. |
75
- | .promises(...args) | Function[] | [] | Adds functions that return promises to the execution queue. |
76
- | .request() | Promise<any[]> | - | Executes all tasks with concurrency control and returns results. |
77
-
78
- ## 🚀 Quick Start
79
-
80
- ```javascript
43
+ ## Quick Start
81
44
 
45
+ ```typescript
82
46
  import vigor from 'vigor-fetch';
83
47
 
84
- const data = await vigor.fetch("https://api.example.com")
85
- .path("/v1/users")
86
- .method("POST")
87
- .headers({ "Authorization": "Bearer TOKEN" })
88
- .body({ name: "Uav1010" })
89
- .count(5) // Retry up to 5 times
90
- .backoff(1.5) // Multiply wait time by 1.5x each failure
91
- .parse("json") // Auto-parse response as JSON
48
+ // Basic GET request
49
+ const data = await vigor
50
+ .fetch('https://api.example.com')
51
+ .path('/users')
92
52
  .request();
93
53
 
54
+ // POST request
55
+ const result = await vigor
56
+ .fetch('https://api.example.com')
57
+ .path('/users')
58
+ .body({ name: 'John', age: 30 })
59
+ .request();
94
60
  ```
95
61
 
96
- ## 🛠️ Advanced Patterns
62
+ ---
63
+
64
+ ## API Reference
65
+
66
+ ### `vigor.fetch(origin?)` — VigorFetch
67
+
68
+ Builds and executes an HTTP request.
69
+
70
+ | Method | Description |
71
+ |---|---|
72
+ | `.origin(str)` | Set the base URL |
73
+ | `.path(str)` | Set the URL path |
74
+ | `.query(obj)` | Set query parameters |
75
+ | `.method(str)` | Set the HTTP method (default: POST if body present, otherwise GET) |
76
+ | `.headers(obj)` | Set request headers |
77
+ | `.body(obj)` | Set the request body (objects/arrays are automatically JSON-serialized) |
78
+ | `.offset(obj)` | Pass options directly to `fetch` |
79
+ | `.maxDelay(ms)` | Maximum wait time per retry (ms) |
80
+ | `.retryHeaders(...str)` | Add custom headers for Rate Limit detection |
81
+ | `.unretry(...int)` | Set HTTP status codes that should not be retried |
82
+ | `.retryConfig(fn)` | Customize the internal VigorRetry configuration |
83
+ | `.parseConfig(fn)` | Customize the internal VigorParse configuration |
84
+ | `.before(...fn)` | Interceptor called before the request |
85
+ | `.after(...fn)` | Interceptor called after response is received (before parsing) |
86
+ | `.result(...fn)` | Interceptor called after parsing is complete |
87
+ | `.onError(...fn)` | Interceptor called on error |
88
+ | `.request()` | Execute the request |
89
+
90
+ **Example**
91
+
92
+ ```typescript
93
+ const data = await vigor
94
+ .fetch('https://api.example.com')
95
+ .path('/items')
96
+ .query({ page: 1, limit: 20 })
97
+ .headers({ Authorization: 'Bearer TOKEN' })
98
+ .retryConfig(r => r.count(3).baseDelay(500))
99
+ .before(async (ctx) => {
100
+ console.log('Request started:', ctx.url);
101
+ })
102
+ .onError(async (ctx, err) => {
103
+ console.error('Error occurred:', err.message);
104
+ })
105
+ .request();
106
+ ```
97
107
 
98
- 1. **Request Templates**
108
+ ---
109
+
110
+ ### `vigor.retry(target, args?, config?)` — VigorRetry
111
+
112
+ Applies retry logic to any async function.
113
+
114
+ | Method | Description |
115
+ |---|---|
116
+ | `.args(...args)` | Set arguments to pass to the target function |
117
+ | `.count(n)` | Maximum number of attempts (default: 5) |
118
+ | `.max(ms)` | Maximum wait time per attempt (default: 10000ms) |
119
+ | `.backoff(factor)` | Exponential backoff multiplier (default: 1.3) |
120
+ | `.baseDelay(ms)` | Base delay between retries (default: 1000ms) |
121
+ | `.jitter(ms)` | Random jitter range added to each delay (default: 500ms) |
122
+ | `.before(...fn)` | Interceptor called before each attempt |
123
+ | `.after(...fn)` | Interceptor called after each successful attempt |
124
+ | `.onRetry(...fn)` | Interceptor called when a retry is triggered |
125
+ | `.onError(...fn)` | Interceptor called on final failure |
126
+ | `.request()` | Execute |
127
+
128
+ **Example**
129
+
130
+ ```typescript
131
+ const result = await vigor
132
+ .retry(async () => {
133
+ const res = await fetch('https://api.example.com/unstable');
134
+ if (!res.ok) throw new Error('Failed');
135
+ return res.json();
136
+ })
137
+ .count(5)
138
+ .baseDelay(1000)
139
+ .backoff(1.5)
140
+ .jitter(300)
141
+ .onRetry(async (ctx) => {
142
+ console.log(`Retry #${ctx.attempt}, waiting ${ctx.wait}ms`);
143
+ })
144
+ .request();
145
+ ```
99
146
 
100
- ```javascript
147
+ ---
101
148
 
102
- import vigor from 'vigor-fetch';
149
+ ### `vigor.parse(response?)` VigorParse
150
+
151
+ Automatically parses a `Response` object based on its Content-Type.
152
+
153
+ | Content-Type | Parsing Method |
154
+ |---|---|
155
+ | `application/json` | `response.json()` |
156
+ | `multipart/form-data` | `response.formData()` |
157
+ | `application/octet-stream` | `response.arrayBuffer()` |
158
+ | `image/*`, `video/*`, `audio/*`, `pdf` | `response.blob()` |
159
+ | anything else | `response.text()` |
160
+
161
+ | Method | Description |
162
+ |---|---|
163
+ | `.original(bool)` | If `true`, returns the raw Response without parsing |
164
+ | `.type(str)` | Force a specific parsing method: `'json'`, `'text'`, `'blob'`, etc. |
165
+ | `.before(...fn)` | Interceptor called before parsing |
166
+ | `.after(...fn)` | Interceptor called after parsing |
167
+ | `.onError(...fn)` | Interceptor called on error |
168
+ | `.request()` | Execute parsing |
169
+
170
+ **Example**
103
171
 
104
- const apiClient = vigor.fetch("https://api.myapp.com")
105
- .headers({ "Content-Type": "application/json" })
106
- .unretry([401, 403, 404]) // Don't retry on these statuses
107
- .max(3000); // 3s timeout per attempt
172
+ ```typescript
173
+ const raw = await fetch('https://api.example.com/data');
108
174
 
109
- const user = await apiClient.path("/me").request();
110
- const settings = await apiClient.path("/settings").request();
175
+ // Auto parsing based on Content-Type
176
+ const parsed = await vigor.parse(raw).request();
111
177
 
178
+ // Force text parsing
179
+ const text = await vigor.parse(raw).type('text').request();
180
+
181
+ // Return the raw Response object
182
+ const original = await vigor.parse(raw).original(true).request();
112
183
  ```
113
184
 
114
- 2. **Batch Processing with Concurrency Limit**
185
+ ---
115
186
 
116
- ```javascript
187
+ ### `vigor.all(config?)` — VigorAll
117
188
 
118
- import vigor from 'vigor-fetch';
189
+ Runs multiple async tasks in parallel with a concurrency limit.
190
+
191
+ | Method | Description |
192
+ |---|---|
193
+ | `.promises(...fn)` | Add Promise factory functions to execute |
194
+ | `.limit(n)` | Maximum number of concurrent tasks (default: 10) |
195
+ | `.jitter(ms)` | Random delay before each task starts (default: 1000ms) |
196
+ | `.before(...fn)` | Interceptor called before execution |
197
+ | `.after(...fn)` | Interceptor called after all tasks complete |
198
+ | `.onError(...fn)` | Interceptor called on error |
199
+ | `.request()` | Execute — always returns an array; failed items are `VigorAllError` instances |
200
+
201
+ **Example**
119
202
 
120
- const tasks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(id => () =>
121
- vigor.fetch("https://api.com").path(`/data/${id}`).request()
203
+ ```typescript
204
+ const tasks = [1, 2, 3, 4, 5].map(id =>
205
+ () => vigor.fetch('https://api.example.com').path(`/items/${id}`).request()
122
206
  );
123
207
 
124
- const results = await vigor.all()
125
- .limit(3) // Max 3 concurrent requests
126
- .jitter(500) // Add up to 500ms random delay between starts
208
+ const results = await vigor
209
+ .all()
127
210
  .promises(...tasks)
211
+ .limit(3) // Run at most 3 tasks concurrently
212
+ .jitter(200) // Add 0–200ms random delay before each task starts
128
213
  .request();
129
214
 
215
+ results.forEach((res, i) => {
216
+ if (res instanceof VigorAllError) {
217
+ console.error(`Task ${i} failed:`, res.message);
218
+ } else {
219
+ console.log(`Task ${i} succeeded:`, res);
220
+ }
221
+ });
130
222
  ```
131
223
 
132
- 3. **Middleware & Hooks**
224
+ ---
133
225
 
134
- ```javascript
226
+ ## Interceptor Context (`ctx`)
135
227
 
136
- import vigor from 'vigor-fetch';
228
+ All interceptor functions receive a `ctx` object as their first argument.
229
+ Returning a plain object from an interceptor merges its keys into `ctx`, making them available to subsequent interceptors.
137
230
 
138
- const api = vigor.fetch("https://api.com")
139
- .beforeRequest((opt) => {
140
- opt.headers = { ...opt.headers, "X-Timestamp": Date.now().toString() };
231
+ ```typescript
232
+ vigor
233
+ .fetch('https://api.example.com')
234
+ .before(async (ctx, option) => {
235
+ // Access ctx.origin, ctx.path, ctx.option, etc.
236
+ return { requestId: 'abc-123' }; // merged into ctx
141
237
  })
142
- .afterResponse((data) => {
143
- return { ...data, receivedAt: new Date() }; // Transform final result
238
+ .after(async (ctx, response) => {
239
+ // ctx.requestId === 'abc-123'
240
+ // ctx.result: current response object
144
241
  })
145
- .onError((err) => {
146
- if (err.status === 404) return { error: "Not Found", fallback: true };
147
- throw err; // Continue throwing if not handled
148
- });
242
+ .request();
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Error Classes
248
+
249
+ | Class | Description |
250
+ |---|---|
251
+ | `VigorError` | Base error class |
252
+ | `VigorFetchError` | Error thrown during fetch |
253
+ | `VigorRetryError` | Error thrown during retry |
254
+ | `VigorParseError` | Error thrown during parsing |
255
+ | `VigorAllError` | Error thrown during parallel execution |
256
+
257
+ All errors share the following shape:
258
+
259
+ ```typescript
260
+ interface VigorErrorOptions {
261
+ type?: string; // Error category
262
+ data?: any; // Related data
263
+ status?: number; // HTTP status code
264
+ response?: any; // Original response
265
+ message?: string; // Custom message
266
+ origin?: string; // Request origin
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Plugins
273
+
274
+ Use `vigor.use(plugin, options?)` to extend behavior.
275
+
276
+ ```typescript
277
+ const authPlugin = (instance, options) => {
278
+ const original = instance.fetch.bind(instance);
279
+ instance.fetch = (origin, config) =>
280
+ original(origin, config).headers({ Authorization: `Bearer ${options.token}` });
281
+ };
282
+
283
+ vigor.use(authPlugin, { token: 'MY_TOKEN' });
284
+
285
+ // Authorization header is now automatically attached to every vigor.fetch() call
286
+ const data = await vigor.fetch('https://api.example.com').path('/me').request();
287
+ ```
149
288
 
150
- ```
289
+ ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigor-fetch",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Smart, zero-dependency HTTP client with self-healing retries for rate-limited servers.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",