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.
- package/README.md +249 -110
- 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
|
|
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
|
-
|
|
6
|
+
---
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
41
|
+
---
|
|
23
42
|
|
|
24
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
---
|
|
101
148
|
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
const
|
|
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
|
-
|
|
185
|
+
---
|
|
115
186
|
|
|
116
|
-
|
|
187
|
+
### `vigor.all(config?)` — VigorAll
|
|
117
188
|
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
125
|
-
.
|
|
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
|
-
|
|
224
|
+
---
|
|
133
225
|
|
|
134
|
-
|
|
226
|
+
## Interceptor Context (`ctx`)
|
|
135
227
|
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
.
|
|
143
|
-
|
|
238
|
+
.after(async (ctx, response) => {
|
|
239
|
+
// ctx.requestId === 'abc-123'
|
|
240
|
+
// ctx.result: current response object
|
|
144
241
|
})
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
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