@firekid/hurl 1.0.7 → 1.1.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/LICENSE +1 -1
- package/README.md +222 -171
- package/dist/index.d.mts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +5 -1
- package/dist/index.mjs +5 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,26 +1,73 @@
|
|
|
1
|
-
# hurl
|
|
1
|
+
# @firekid/hurl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmjs.com/package/@firekid/hurl)
|
|
4
|
+
[](https://npmjs.com/package/@firekid/hurl)
|
|
5
|
+
[](https://bundlephobia.com/package/@firekid/hurl)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://github.com/firekid-is-him/hurl/actions)
|
|
9
|
+
[](https://github.com/firekid-is-him/hurl/stargazers)
|
|
10
|
+
[](https://hurl.firekidofficial.name.ng)
|
|
11
|
+
|
|
12
|
+
**`@firekid/hurl`** is a modern, zero-dependency HTTP client for Node.js 18+, Cloudflare Workers, Vercel Edge Functions, Deno, and Bun — built on native fetch with retries, interceptors, auth helpers, in-memory caching, request deduplication, and full TypeScript support.
|
|
4
13
|
|
|
5
14
|
```bash
|
|
6
15
|
npm install @firekid/hurl
|
|
7
16
|
```
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
---
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Why hurl?
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
Most HTTP clients make you choose between features and bundle size, or between Node.js support and edge compatibility. `@firekid/hurl` does neither.
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
```ts
|
|
25
|
+
import hurl from '@firekid/hurl'
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
// Retry automatically on failure
|
|
28
|
+
const res = await hurl.get('https://api.example.com/users', { retry: 3 })
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
// Auth, timeout, caching — all in one call
|
|
31
|
+
const data = await hurl.get('/users', {
|
|
32
|
+
auth: { type: 'bearer', token: process.env.API_TOKEN },
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
cache: { ttl: 60000 },
|
|
35
|
+
})
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
// Parallel requests
|
|
38
|
+
const [users, posts] = await hurl.all([
|
|
39
|
+
hurl.get('/users'),
|
|
40
|
+
hurl.get('/posts'),
|
|
41
|
+
])
|
|
42
|
+
```
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Comparison
|
|
47
|
+
|
|
48
|
+
| Feature | **hurl** | axios | ky | got | node-fetch |
|
|
49
|
+
|---|:---:|:---:|:---:|:---:|:---:|
|
|
50
|
+
| Zero dependencies | ✅ | ❌ | ✅ | ❌ | ✅ |
|
|
51
|
+
| Bundle size | **~9KB** | ~35KB | ~5KB | ~45KB | ~8KB |
|
|
52
|
+
| Node.js 18+ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
+
| Cloudflare Workers | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
54
|
+
| Vercel Edge | ✅ | ❌ | ✅ | ❌ | ❌ |
|
|
55
|
+
| Deno / Bun | ✅ | ⚠️ | ✅ | ⚠️ | ❌ |
|
|
56
|
+
| Built-in retries | ✅ | ❌ | ✅ | ✅ | ❌ |
|
|
57
|
+
| Interceptors | ✅ | ✅ | ✅ | ❌ | ❌ |
|
|
58
|
+
| Auth helpers | ✅ | ⚠️ | ❌ | ❌ | ❌ |
|
|
59
|
+
| In-memory cache | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
60
|
+
| Request deduplication | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
61
|
+
| Upload progress | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
62
|
+
| Download progress | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
63
|
+
| Proxy support | ✅ | ✅ | ❌ | ✅ | ❌ |
|
|
64
|
+
| CommonJS + ESM | ✅ | ✅ | ❌ | ❌ | ✅ |
|
|
65
|
+
| TypeScript (built-in) | ✅ | ⚠️ | ✅ | ✅ | ⚠️ |
|
|
66
|
+
| Throws on 4xx/5xx | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
67
|
+
|
|
68
|
+
> ✅ Full support ⚠️ Partial / via plugin ❌ Not supported
|
|
69
|
+
|
|
70
|
+
---
|
|
24
71
|
|
|
25
72
|
## Installation
|
|
26
73
|
|
|
@@ -30,6 +77,8 @@ yarn add @firekid/hurl
|
|
|
30
77
|
pnpm add @firekid/hurl
|
|
31
78
|
```
|
|
32
79
|
|
|
80
|
+
---
|
|
81
|
+
|
|
33
82
|
## Quick Start
|
|
34
83
|
|
|
35
84
|
```ts
|
|
@@ -45,6 +94,8 @@ res.timing // { start, end, duration }
|
|
|
45
94
|
res.fromCache // boolean
|
|
46
95
|
```
|
|
47
96
|
|
|
97
|
+
---
|
|
98
|
+
|
|
48
99
|
## HTTP Methods
|
|
49
100
|
|
|
50
101
|
```ts
|
|
@@ -58,6 +109,8 @@ hurl.options<T>(url, options?)
|
|
|
58
109
|
hurl.request<T>(url, options?)
|
|
59
110
|
```
|
|
60
111
|
|
|
112
|
+
---
|
|
113
|
+
|
|
61
114
|
## Global Defaults
|
|
62
115
|
|
|
63
116
|
```ts
|
|
@@ -72,61 +125,33 @@ hurl.defaults.get()
|
|
|
72
125
|
hurl.defaults.reset()
|
|
73
126
|
```
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
All methods accept a `HurlRequestOptions` object.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
type HurlRequestOptions = {
|
|
81
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
82
|
-
headers?: Record<string, string>
|
|
83
|
-
body?: unknown
|
|
84
|
-
query?: Record<string, string | number | boolean>
|
|
85
|
-
timeout?: number
|
|
86
|
-
retry?: RetryConfig | number
|
|
87
|
-
auth?: AuthConfig
|
|
88
|
-
proxy?: ProxyConfig
|
|
89
|
-
cache?: CacheConfig
|
|
90
|
-
signal?: AbortSignal
|
|
91
|
-
followRedirects?: boolean
|
|
92
|
-
maxRedirects?: number
|
|
93
|
-
onUploadProgress?: ProgressCallback
|
|
94
|
-
onDownloadProgress?: ProgressCallback
|
|
95
|
-
stream?: boolean
|
|
96
|
-
throwOnError?: boolean
|
|
97
|
-
debug?: boolean
|
|
98
|
-
requestId?: string
|
|
99
|
-
deduplicate?: boolean
|
|
100
|
-
}
|
|
101
|
-
```
|
|
128
|
+
---
|
|
102
129
|
|
|
103
130
|
## Authentication
|
|
104
131
|
|
|
105
132
|
```ts
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})
|
|
133
|
+
// Bearer token
|
|
134
|
+
hurl.defaults.set({ auth: { type: 'bearer', token: 'my-token' } })
|
|
109
135
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
136
|
+
// Basic auth
|
|
137
|
+
hurl.defaults.set({ auth: { type: 'basic', username: 'admin', password: 'secret' } })
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
})
|
|
139
|
+
// API key (header)
|
|
140
|
+
hurl.defaults.set({ auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' } })
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})
|
|
142
|
+
// API key (query param)
|
|
143
|
+
hurl.defaults.set({ auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' } })
|
|
121
144
|
```
|
|
122
145
|
|
|
123
|
-
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Retry & Backoff
|
|
124
149
|
|
|
125
150
|
```ts
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
})
|
|
151
|
+
// Simple — retry 3 times with exponential backoff
|
|
152
|
+
await hurl.get('/users', { retry: 3 })
|
|
129
153
|
|
|
154
|
+
// Full config
|
|
130
155
|
await hurl.get('/users', {
|
|
131
156
|
retry: {
|
|
132
157
|
count: 3,
|
|
@@ -137,9 +162,11 @@ await hurl.get('/users', {
|
|
|
137
162
|
})
|
|
138
163
|
```
|
|
139
164
|
|
|
140
|
-
|
|
165
|
+
Retries are not triggered for abort errors. If no `on` array is provided, retries fire on network errors, timeout errors, and any 5xx status.
|
|
166
|
+
|
|
167
|
+
---
|
|
141
168
|
|
|
142
|
-
## Timeout
|
|
169
|
+
## Timeout & Abort
|
|
143
170
|
|
|
144
171
|
```ts
|
|
145
172
|
await hurl.get('/users', { timeout: 5000 })
|
|
@@ -149,9 +176,12 @@ setTimeout(() => controller.abort(), 3000)
|
|
|
149
176
|
await hurl.get('/users', { signal: controller.signal })
|
|
150
177
|
```
|
|
151
178
|
|
|
179
|
+
---
|
|
180
|
+
|
|
152
181
|
## Interceptors
|
|
153
182
|
|
|
154
183
|
```ts
|
|
184
|
+
// Request interceptor
|
|
155
185
|
const remove = hurl.interceptors.request.use((url, options) => {
|
|
156
186
|
return {
|
|
157
187
|
url,
|
|
@@ -161,63 +191,36 @@ const remove = hurl.interceptors.request.use((url, options) => {
|
|
|
161
191
|
},
|
|
162
192
|
}
|
|
163
193
|
})
|
|
194
|
+
remove() // unregister
|
|
164
195
|
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
// Response interceptor
|
|
167
197
|
hurl.interceptors.response.use((response) => {
|
|
168
198
|
console.log(response.status, response.timing.duration)
|
|
169
199
|
return response
|
|
170
200
|
})
|
|
171
201
|
|
|
202
|
+
// Error interceptor
|
|
172
203
|
hurl.interceptors.error.use((error) => {
|
|
173
204
|
if (error.status === 401) redirectToLogin()
|
|
174
205
|
return error
|
|
175
206
|
})
|
|
176
207
|
|
|
208
|
+
// Clear all
|
|
177
209
|
hurl.interceptors.request.clear()
|
|
178
210
|
hurl.interceptors.response.clear()
|
|
179
211
|
hurl.interceptors.error.clear()
|
|
180
212
|
```
|
|
181
213
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
const form = new FormData()
|
|
186
|
-
form.append('file', file)
|
|
187
|
-
|
|
188
|
-
await hurl.post('/upload', form, {
|
|
189
|
-
onUploadProgress: ({ loaded, total, percent }) => {
|
|
190
|
-
console.log(`${percent}%`)
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Download Progress
|
|
196
|
-
|
|
197
|
-
```ts
|
|
198
|
-
await hurl.get('/large-file', {
|
|
199
|
-
onDownloadProgress: ({ loaded, total, percent }) => {
|
|
200
|
-
console.log(`${percent}%`)
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
```
|
|
214
|
+
---
|
|
204
215
|
|
|
205
216
|
## Caching
|
|
206
217
|
|
|
207
|
-
Caching
|
|
218
|
+
Caching applies to GET requests only. Responses are stored in memory with a TTL in milliseconds.
|
|
208
219
|
|
|
209
220
|
```ts
|
|
210
|
-
await hurl.get('/users', {
|
|
211
|
-
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
await hurl.get('/users', {
|
|
215
|
-
cache: { ttl: 60000, key: 'all-users' }
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
await hurl.get('/users', {
|
|
219
|
-
cache: { ttl: 60000, bypass: true }
|
|
220
|
-
})
|
|
221
|
+
await hurl.get('/users', { cache: { ttl: 60000 } })
|
|
222
|
+
await hurl.get('/users', { cache: { ttl: 60000, key: 'all-users' } })
|
|
223
|
+
await hurl.get('/users', { cache: { ttl: 60000, bypass: true } })
|
|
221
224
|
```
|
|
222
225
|
|
|
223
226
|
```ts
|
|
@@ -231,6 +234,8 @@ invalidateCache('https://api.example.com/users')
|
|
|
231
234
|
invalidateCache('all-users') // if you used a custom cache key
|
|
232
235
|
```
|
|
233
236
|
|
|
237
|
+
---
|
|
238
|
+
|
|
234
239
|
## Request Deduplication
|
|
235
240
|
|
|
236
241
|
When `deduplicate` is true and the same GET URL is called multiple times simultaneously, only one network request is made.
|
|
@@ -240,8 +245,34 @@ const [a, b] = await Promise.all([
|
|
|
240
245
|
hurl.get('/users', { deduplicate: true }),
|
|
241
246
|
hurl.get('/users', { deduplicate: true }),
|
|
242
247
|
])
|
|
248
|
+
// only one network request fired
|
|
243
249
|
```
|
|
244
250
|
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Upload & Download Progress
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// Upload
|
|
257
|
+
const form = new FormData()
|
|
258
|
+
form.append('file', file)
|
|
259
|
+
|
|
260
|
+
await hurl.post('/upload', form, {
|
|
261
|
+
onUploadProgress: ({ loaded, total, percent }) => {
|
|
262
|
+
console.log(`Uploading: ${percent}%`)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Download
|
|
267
|
+
await hurl.get('/large-file', {
|
|
268
|
+
onDownloadProgress: ({ loaded, total, percent }) => {
|
|
269
|
+
console.log(`Downloading: ${percent}%`)
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
245
276
|
## Proxy
|
|
246
277
|
|
|
247
278
|
Native fetch does not support programmatic proxy configuration out of the box. Proxy support depends on your Node.js version:
|
|
@@ -273,6 +304,9 @@ NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 node app.js
|
|
|
273
304
|
|
|
274
305
|
The `proxy` option in `HurlRequestOptions` is reserved for a future release where this will be handled automatically.
|
|
275
306
|
|
|
307
|
+
---
|
|
308
|
+
---
|
|
309
|
+
|
|
276
310
|
## Parallel Requests
|
|
277
311
|
|
|
278
312
|
```ts
|
|
@@ -282,6 +316,8 @@ const [users, posts] = await hurl.all([
|
|
|
282
316
|
])
|
|
283
317
|
```
|
|
284
318
|
|
|
319
|
+
---
|
|
320
|
+
|
|
285
321
|
## Isolated Instances
|
|
286
322
|
|
|
287
323
|
```ts
|
|
@@ -294,26 +330,19 @@ const api = hurl.create({
|
|
|
294
330
|
|
|
295
331
|
await api.get('/users')
|
|
296
332
|
|
|
333
|
+
// Extend with overrides
|
|
297
334
|
const adminApi = api.extend({
|
|
298
335
|
headers: { 'x-role': 'admin' }
|
|
299
336
|
})
|
|
300
337
|
```
|
|
301
338
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
## Debug Mode
|
|
305
|
-
|
|
306
|
-
Logs the full request (method, url, headers, body, query, timeout, retry config) and response (status, timing, headers, data) to the console. Errors and retries are also logged.
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
await hurl.get('/users', { debug: true })
|
|
310
|
-
```
|
|
339
|
+
---
|
|
311
340
|
|
|
312
341
|
## Error Handling
|
|
313
342
|
|
|
314
|
-
`hurl` throws a `HurlError` on HTTP errors (4xx
|
|
343
|
+
`hurl` throws a `HurlError` on HTTP errors (4xx/5xx), network failures, timeouts, aborts, and parse failures. It never resolves silently on bad status codes.
|
|
315
344
|
|
|
316
|
-
If you want to handle 4xx/5xx responses without a try/catch, set `throwOnError: false` — the response
|
|
345
|
+
If you want to handle 4xx/5xx responses yourself without a try/catch, set `throwOnError: false` — the response will resolve normally and you can check `res.status` yourself.
|
|
317
346
|
|
|
318
347
|
```ts
|
|
319
348
|
const res = await hurl.get('/users', { throwOnError: false })
|
|
@@ -340,18 +369,32 @@ try {
|
|
|
340
369
|
}
|
|
341
370
|
```
|
|
342
371
|
|
|
372
|
+
---
|
|
373
|
+
|
|
343
374
|
## TypeScript
|
|
344
375
|
|
|
345
376
|
```ts
|
|
346
377
|
type User = { id: number; name: string }
|
|
347
378
|
|
|
348
379
|
const res = await hurl.get<User[]>('/users')
|
|
349
|
-
res.data
|
|
380
|
+
res.data // User[]
|
|
350
381
|
|
|
351
382
|
const created = await hurl.post<User>('/users', { name: 'John' })
|
|
352
|
-
created.data.id
|
|
383
|
+
created.data.id // number
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Debug Mode
|
|
389
|
+
|
|
390
|
+
Logs the full request (method, url, headers, body, query, timeout, retry config) and response (status, timing, headers, data) to the console. Errors and retries are also logged.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
await hurl.get('/users', { debug: true })
|
|
353
394
|
```
|
|
354
395
|
|
|
396
|
+
---
|
|
397
|
+
|
|
355
398
|
## Response Shape
|
|
356
399
|
|
|
357
400
|
```ts
|
|
@@ -370,88 +413,96 @@ type HurlResponse<T> = {
|
|
|
370
413
|
}
|
|
371
414
|
```
|
|
372
415
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`hurl` runs anywhere the Fetch API is available.
|
|
376
|
-
|
|
377
|
-
- Node.js 18 and above
|
|
378
|
-
- Cloudflare Workers
|
|
379
|
-
- Vercel Edge Functions
|
|
380
|
-
- Deno
|
|
381
|
-
- Bun
|
|
382
|
-
|
|
383
|
-
Exports both ESM (`import`) and CommonJS (`require`).
|
|
416
|
+
---
|
|
384
417
|
|
|
385
|
-
##
|
|
386
|
-
|
|
387
|
-
### hurl.get(url, options?)
|
|
388
|
-
Sends a GET request. Returns `Promise<HurlResponse<T>>`.
|
|
389
|
-
|
|
390
|
-
### hurl.post(url, body?, options?)
|
|
391
|
-
Sends a POST request. Body is auto-serialized to JSON if it is a plain object. Returns `Promise<HurlResponse<T>>`.
|
|
392
|
-
|
|
393
|
-
### hurl.put(url, body?, options?)
|
|
394
|
-
Sends a PUT request. Returns `Promise<HurlResponse<T>>`.
|
|
395
|
-
|
|
396
|
-
### hurl.patch(url, body?, options?)
|
|
397
|
-
Sends a PATCH request. Returns `Promise<HurlResponse<T>>`.
|
|
398
|
-
|
|
399
|
-
### hurl.delete(url, options?)
|
|
400
|
-
Sends a DELETE request. Returns `Promise<HurlResponse<T>>`.
|
|
401
|
-
|
|
402
|
-
### hurl.head(url, options?)
|
|
403
|
-
Sends a HEAD request. Returns `Promise<HurlResponse<void>>`.
|
|
418
|
+
## Request Options
|
|
404
419
|
|
|
405
|
-
|
|
406
|
-
|
|
420
|
+
```ts
|
|
421
|
+
type HurlRequestOptions = {
|
|
422
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
423
|
+
headers?: Record<string, string>
|
|
424
|
+
body?: unknown
|
|
425
|
+
query?: Record<string, string | number | boolean>
|
|
426
|
+
timeout?: number
|
|
427
|
+
retry?: RetryConfig | number
|
|
428
|
+
auth?: AuthConfig
|
|
429
|
+
proxy?: ProxyConfig
|
|
430
|
+
cache?: CacheConfig
|
|
431
|
+
signal?: AbortSignal
|
|
432
|
+
followRedirects?: boolean
|
|
433
|
+
maxRedirects?: number
|
|
434
|
+
onUploadProgress?: ProgressCallback
|
|
435
|
+
onDownloadProgress?: ProgressCallback
|
|
436
|
+
stream?: boolean
|
|
437
|
+
debug?: boolean
|
|
438
|
+
requestId?: string
|
|
439
|
+
deduplicate?: boolean
|
|
440
|
+
}
|
|
441
|
+
```
|
|
407
442
|
|
|
408
|
-
|
|
409
|
-
Sends a request with the method specified in options. Defaults to GET. Returns `Promise<HurlResponse<T>>`.
|
|
443
|
+
---
|
|
410
444
|
|
|
411
|
-
|
|
412
|
-
Runs an array of requests in parallel. Returns a promise that resolves when all requests complete. Equivalent to `Promise.all`.
|
|
445
|
+
## Environment Support
|
|
413
446
|
|
|
414
|
-
|
|
415
|
-
Creates a new isolated instance with its own defaults, interceptors, and state. Does not inherit anything from the parent instance.
|
|
447
|
+
`@firekid/hurl` runs anywhere the Fetch API is available. No adapters, no polyfills needed.
|
|
416
448
|
|
|
417
|
-
|
|
418
|
-
|
|
449
|
+
| Runtime | Support |
|
|
450
|
+
|---|:---:|
|
|
451
|
+
| Node.js 18+ | ✅ |
|
|
452
|
+
| Cloudflare Workers | ✅ |
|
|
453
|
+
| Vercel Edge Functions | ✅ |
|
|
454
|
+
| Deno | ✅ |
|
|
455
|
+
| Bun | ✅ |
|
|
419
456
|
|
|
420
|
-
|
|
421
|
-
Sets global defaults for the current instance. Merged into every request.
|
|
457
|
+
Exports both ESM (`import`) and CommonJS (`require`).
|
|
422
458
|
|
|
423
|
-
|
|
424
|
-
Returns the current defaults object.
|
|
459
|
+
---
|
|
425
460
|
|
|
426
|
-
|
|
427
|
-
Resets defaults to the values provided when the instance was created.
|
|
461
|
+
## Why Not Axios?
|
|
428
462
|
|
|
429
|
-
|
|
430
|
-
Registers a request interceptor. Returns a function that removes the interceptor when called.
|
|
463
|
+
**axios** is 35KB, has no native edge runtime support, no built-in retry, no deduplication, and carries `XMLHttpRequest` baggage from a different era of the web.
|
|
431
464
|
|
|
432
|
-
|
|
433
|
-
Registers a response interceptor. Returns a function that removes the interceptor when called.
|
|
465
|
+
**got** dropped CommonJS in v12 — if your project uses `require()`, you're stuck on an old version.
|
|
434
466
|
|
|
435
|
-
|
|
436
|
-
Registers an error interceptor. Returns a function that removes the interceptor when called.
|
|
467
|
+
**ky** is browser-first. No Node.js, no proxy, no streaming.
|
|
437
468
|
|
|
438
|
-
|
|
439
|
-
Clears the entire in-memory response cache.
|
|
469
|
+
**node-fetch** is a polyfill. Node.js has had native fetch since v18. You don't need it anymore.
|
|
440
470
|
|
|
441
|
-
|
|
442
|
-
import { clearCache } from '@firekid/hurl'
|
|
443
|
-
clearCache()
|
|
444
|
-
```
|
|
471
|
+
**request** has been deprecated since 2020.
|
|
445
472
|
|
|
446
|
-
|
|
447
|
-
Removes a single entry from the in-memory cache by URL or custom cache key.
|
|
473
|
+
**`@firekid/hurl`** is built for how Node.js and the edge work today — native fetch, zero dependencies, everything included, works everywhere.
|
|
448
474
|
|
|
449
|
-
|
|
450
|
-
import { invalidateCache } from '@firekid/hurl'
|
|
451
|
-
invalidateCache('https://api.example.com/users')
|
|
452
|
-
invalidateCache('all-users') // if you used a custom cache key
|
|
453
|
-
```
|
|
475
|
+
---
|
|
454
476
|
|
|
455
|
-
##
|
|
477
|
+
## API Reference
|
|
456
478
|
|
|
457
|
-
|
|
479
|
+
| Method | Description |
|
|
480
|
+
|---|---|
|
|
481
|
+
| `hurl.get(url, options?)` | GET request → `Promise<HurlResponse<T>>` |
|
|
482
|
+
| `hurl.post(url, body?, options?)` | POST request, body auto-serialized to JSON |
|
|
483
|
+
| `hurl.put(url, body?, options?)` | PUT request |
|
|
484
|
+
| `hurl.patch(url, body?, options?)` | PATCH request |
|
|
485
|
+
| `hurl.delete(url, options?)` | DELETE request |
|
|
486
|
+
| `hurl.head(url, options?)` | HEAD request → `Promise<HurlResponse<void>>` |
|
|
487
|
+
| `hurl.options(url, options?)` | OPTIONS request |
|
|
488
|
+
| `hurl.request(url, options?)` | Generic request, method from options |
|
|
489
|
+
| `hurl.all(requests)` | Run requests in parallel |
|
|
490
|
+
| `hurl.create(defaults?)` | New isolated instance |
|
|
491
|
+
| `hurl.extend(defaults?)` | New instance inheriting current defaults |
|
|
492
|
+
| `hurl.defaults.set(defaults)` | Set global defaults |
|
|
493
|
+
| `hurl.defaults.get()` | Get current defaults |
|
|
494
|
+
| `hurl.defaults.reset()` | Reset defaults to instance creation values |
|
|
495
|
+
| `hurl.interceptors.request.use(fn)` | Register request interceptor |
|
|
496
|
+
| `hurl.interceptors.response.use(fn)` | Register response interceptor |
|
|
497
|
+
| `hurl.interceptors.error.use(fn)` | Register error interceptor |
|
|
498
|
+
| `clearCache()` | Clear in-memory response cache |
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Contributors
|
|
503
|
+
|
|
504
|
+
[](https://github.com/HeavstalTech) **[HeavstalTech](https://github.com/HeavstalTech)** — signal fix, cache hardening, test suite
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
Built with ♥️ by [Firekid](https://github.com/Firekid-is-him) · [MIT License](./LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -30,11 +30,35 @@ type CacheConfig = {
|
|
|
30
30
|
key?: string;
|
|
31
31
|
bypass?: boolean;
|
|
32
32
|
};
|
|
33
|
+
type CircuitBreakerConfig = {
|
|
34
|
+
threshold: number;
|
|
35
|
+
cooldown: number;
|
|
36
|
+
key?: string;
|
|
37
|
+
fallback?: () => unknown;
|
|
38
|
+
};
|
|
33
39
|
type ProgressCallback = (e: {
|
|
34
40
|
loaded: number;
|
|
35
41
|
total: number;
|
|
36
42
|
percent: number;
|
|
37
43
|
}) => void;
|
|
44
|
+
type SSEEvent = {
|
|
45
|
+
data: string;
|
|
46
|
+
event: string;
|
|
47
|
+
id: string;
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
type SSEOptions = {
|
|
51
|
+
method?: 'GET' | 'POST';
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
query?: Record<string, string | number | boolean>;
|
|
55
|
+
auth?: AuthConfig;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onOpen?: () => void;
|
|
58
|
+
onMessage: (event: SSEEvent) => void;
|
|
59
|
+
onError?: (error: Error) => void;
|
|
60
|
+
onDone?: () => void;
|
|
61
|
+
};
|
|
38
62
|
type HurlRequestOptions = {
|
|
39
63
|
method?: Method;
|
|
40
64
|
headers?: Record<string, string>;
|
|
@@ -45,6 +69,7 @@ type HurlRequestOptions = {
|
|
|
45
69
|
auth?: AuthConfig;
|
|
46
70
|
proxy?: ProxyConfig;
|
|
47
71
|
cache?: CacheConfig;
|
|
72
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
48
73
|
signal?: AbortSignal;
|
|
49
74
|
followRedirects?: boolean;
|
|
50
75
|
onUploadProgress?: ProgressCallback;
|
|
@@ -80,7 +105,7 @@ type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<
|
|
|
80
105
|
};
|
|
81
106
|
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
107
|
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
-
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
108
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR' | 'CIRCUIT_OPEN';
|
|
84
109
|
declare class HurlError extends Error {
|
|
85
110
|
type: HurlErrorType;
|
|
86
111
|
status?: number;
|
|
@@ -109,6 +134,9 @@ type HurlInstance = {
|
|
|
109
134
|
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
135
|
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
136
|
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
137
|
+
sse(url: string, options: SSEOptions): {
|
|
138
|
+
close(): void;
|
|
139
|
+
};
|
|
112
140
|
all<T extends unknown[]>(requests: {
|
|
113
141
|
[K in keyof T]: Promise<T[K]>;
|
|
114
142
|
}): Promise<T>;
|
|
@@ -135,9 +163,16 @@ type HurlInstance = {
|
|
|
135
163
|
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
164
|
};
|
|
137
165
|
|
|
166
|
+
declare function invalidateCache(key: string): void;
|
|
138
167
|
declare function clearCache(): void;
|
|
139
168
|
|
|
169
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
170
|
+
declare function getCircuitStats(key: string): {
|
|
171
|
+
state: CircuitState;
|
|
172
|
+
failures: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
140
175
|
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
176
|
declare const hurl: HurlInstance;
|
|
142
177
|
|
|
143
|
-
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
|
178
|
+
export { type CircuitBreakerConfig, type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, type SSEEvent, type SSEOptions, clearCache, createInstance, hurl as default, getCircuitStats, invalidateCache };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,11 +30,35 @@ type CacheConfig = {
|
|
|
30
30
|
key?: string;
|
|
31
31
|
bypass?: boolean;
|
|
32
32
|
};
|
|
33
|
+
type CircuitBreakerConfig = {
|
|
34
|
+
threshold: number;
|
|
35
|
+
cooldown: number;
|
|
36
|
+
key?: string;
|
|
37
|
+
fallback?: () => unknown;
|
|
38
|
+
};
|
|
33
39
|
type ProgressCallback = (e: {
|
|
34
40
|
loaded: number;
|
|
35
41
|
total: number;
|
|
36
42
|
percent: number;
|
|
37
43
|
}) => void;
|
|
44
|
+
type SSEEvent = {
|
|
45
|
+
data: string;
|
|
46
|
+
event: string;
|
|
47
|
+
id: string;
|
|
48
|
+
retry?: number;
|
|
49
|
+
};
|
|
50
|
+
type SSEOptions = {
|
|
51
|
+
method?: 'GET' | 'POST';
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
body?: unknown;
|
|
54
|
+
query?: Record<string, string | number | boolean>;
|
|
55
|
+
auth?: AuthConfig;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
onOpen?: () => void;
|
|
58
|
+
onMessage: (event: SSEEvent) => void;
|
|
59
|
+
onError?: (error: Error) => void;
|
|
60
|
+
onDone?: () => void;
|
|
61
|
+
};
|
|
38
62
|
type HurlRequestOptions = {
|
|
39
63
|
method?: Method;
|
|
40
64
|
headers?: Record<string, string>;
|
|
@@ -45,6 +69,7 @@ type HurlRequestOptions = {
|
|
|
45
69
|
auth?: AuthConfig;
|
|
46
70
|
proxy?: ProxyConfig;
|
|
47
71
|
cache?: CacheConfig;
|
|
72
|
+
circuitBreaker?: CircuitBreakerConfig;
|
|
48
73
|
signal?: AbortSignal;
|
|
49
74
|
followRedirects?: boolean;
|
|
50
75
|
onUploadProgress?: ProgressCallback;
|
|
@@ -80,7 +105,7 @@ type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<
|
|
|
80
105
|
};
|
|
81
106
|
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
107
|
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
-
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
108
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR' | 'CIRCUIT_OPEN';
|
|
84
109
|
declare class HurlError extends Error {
|
|
85
110
|
type: HurlErrorType;
|
|
86
111
|
status?: number;
|
|
@@ -109,6 +134,9 @@ type HurlInstance = {
|
|
|
109
134
|
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
135
|
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
136
|
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
137
|
+
sse(url: string, options: SSEOptions): {
|
|
138
|
+
close(): void;
|
|
139
|
+
};
|
|
112
140
|
all<T extends unknown[]>(requests: {
|
|
113
141
|
[K in keyof T]: Promise<T[K]>;
|
|
114
142
|
}): Promise<T>;
|
|
@@ -135,9 +163,16 @@ type HurlInstance = {
|
|
|
135
163
|
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
164
|
};
|
|
137
165
|
|
|
166
|
+
declare function invalidateCache(key: string): void;
|
|
138
167
|
declare function clearCache(): void;
|
|
139
168
|
|
|
169
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
170
|
+
declare function getCircuitStats(key: string): {
|
|
171
|
+
state: CircuitState;
|
|
172
|
+
failures: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
140
175
|
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
176
|
declare const hurl: HurlInstance;
|
|
142
177
|
|
|
143
|
-
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
|
178
|
+
export { type CircuitBreakerConfig, type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, type SSEEvent, type SSEOptions, clearCache, createInstance, hurl as default, getCircuitStats, invalidateCache };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var B=Object.defineProperty;var Te=Object.getOwnPropertyDescriptor;var He=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var Oe=(r,e)=>{for(var t in e)B(r,t,{get:e[t],enumerable:!0})},xe=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of He(e))!we.call(r,o)&&o!==t&&B(r,o,{get:()=>e[o],enumerable:!(n=Te(e,o))||n.enumerable});return r};var Ce=r=>xe(B({},"__esModule",{value:!0}),r);var _e={};Oe(_e,{HurlError:()=>d,clearCache:()=>ue,createInstance:()=>v,default:()=>Be,getCircuitStats:()=>me,invalidateCache:()=>ie});module.exports=Ce(_e);var d=class extends Error{constructor(e){super(e.message),this.name="HurlError",this.type=e.type,this.status=e.status,this.statusText=e.statusText,this.data=e.data,this.headers=e.headers,this.requestId=e.requestId,this.retries=e.retries??0}};function j(r){return new d({message:`HTTP ${r.status}: ${r.statusText}`,type:"HTTP_ERROR",...r})}function z(r,e){return new d({message:r,type:"NETWORK_ERROR",requestId:e})}function X(r,e){return new d({message:`Request timed out after ${r}ms`,type:"TIMEOUT_ERROR",requestId:e})}function J(r){return new d({message:"Request was aborted",type:"ABORT_ERROR",requestId:r})}function _(r,e){return new d({message:`Failed to parse response: ${r}`,type:"PARSE_ERROR",requestId:e})}function Y(r,e){return new d({message:`Circuit breaker is open for "${r}"`,type:"CIRCUIT_OPEN",requestId:e})}async function V(r,e){let t=r.body?.getReader(),n=parseInt(r.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let o=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;o.push(a),i+=a.byteLength,e({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let l=new Uint8Array(i),s=0;for(let u of o)l.set(u,s),s+=u.byteLength;return l.buffer}function Z(r,e){let t=0;typeof r=="string"?t=new TextEncoder().encode(r).byteLength:r instanceof ArrayBuffer?t=r.byteLength:r instanceof Blob&&(t=r.size);let n=0;return(r instanceof ReadableStream?r:new Response(r).body).pipeThrough(new TransformStream({transform(i,l){n+=i.byteLength,e({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),l.enqueue(i)}}))}function N(r){let e={};return r.forEach((t,n)=>{e[n]=t}),e}async function Q(r,e,t,n,o){if(t==="HEAD"||r.status===204||r.headers.get("content-length")==="0")return null;if(n)return r.body;let i=r.headers.get("content-type")??"",l=i.includes("application/octet-stream")||i.includes("image/")||i.includes("video/")||i.includes("audio/");if(o&&l)try{return await V(r,o)}catch(s){throw _(s.message,e)}try{return i.includes("application/json")?await r.json():i.includes("text/")?await r.text():l?await r.arrayBuffer():await r.text()}catch(s){throw _(s.message,e)}}function ee(r,e,t,n){let o=Date.now();return{data:r,status:e.status,statusText:e.statusText,headers:N(e.headers),requestId:t,timing:{start:n,end:o,duration:o-n},fromCache:!1}}function Pe(r){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(r).toString("base64"):btoa(encodeURIComponent(r).replace(/%([0-9A-F]{2})/g,(e,t)=>String.fromCharCode(parseInt(t,16))))}function q(r,e,t){if(t.type==="bearer"&&(r.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=Pe(`${t.username}:${t.password}`);r.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?e[t.key]=t.value:r[t.key]=t.value)}function re(r){return r==null?null:typeof r=="number"?{count:r,delay:300,backoff:"exponential"}:r}function te(r,e,t){return t>=e.count||r.type==="ABORT_ERROR"?!1:e.on&&r.status?e.on.includes(r.status):!!(r.type==="NETWORK_ERROR"||r.type==="TIMEOUT_ERROR"||r.status&&r.status>=500)}async function ne(r,e){let t=r.delay??300,n=r.backoff==="exponential"?t*Math.pow(2,e):t*(e+1);await new Promise(o=>setTimeout(o,n))}var E=new Map,Ie=1e3;function L(r,e){return e?.key??r}function oe(r){let e=E.get(r);if(!e)return null;if(Date.now()>e.expiresAt)return E.delete(r),null;let t=e.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...e.response,data:t,fromCache:!0}}function se(r,e,t){if(E.size>=Ie&&!E.has(r)){let n=E.keys().next().value;n!==void 0&&E.delete(n)}E.set(r,{response:e,expiresAt:Date.now()+t.ttl})}function ie(r){E.delete(r)}function ue(){E.clear()}var $=new Map;function ae(r){return $.get(r)??null}function le(r,e){$.set(r,e),e.finally(()=>$.delete(r))}function ce(r,e){console.group(`[hurl] \u2192 ${e.method??"GET"} ${r}`),e.headers&&Object.keys(e.headers).length>0&&console.log("headers:",e.headers),e.query&&console.log("query:",e.query),e.body&&console.log("body:",e.body),e.timeout&&console.log("timeout:",e.timeout),e.retry&&console.log("retry:",e.retry),console.groupEnd()}function F(r){let e=r.status>=400?"\u{1F534}":r.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${e} ${r.status} ${r.statusText} (${r.timing.duration}ms)`),console.log("requestId:",r.requestId),r.fromCache&&console.log("served from cache"),console.log("headers:",r.headers),console.log("data:",r.data),console.groupEnd()}function fe(r){console.group("[hurl] \u{1F534} Error"),console.error(r),console.groupEnd()}var M=new Map;function A(r){return M.has(r)||M.set(r,{state:"CLOSED",failures:0,openedAt:0}),M.get(r)}function pe(r,e){let t=A(r);return t.state==="OPEN"?Date.now()-t.openedAt>=e.cooldown?(t.state="HALF_OPEN","HALF_OPEN"):"OPEN":t.state}function de(r){let e=A(r);e.state="CLOSED",e.failures=0}function ge(r,e){let t=A(r);t.failures+=1,t.failures>=e.threshold&&(t.state="OPEN",t.openedAt=Date.now())}function me(r){let e=A(r);return{state:e.state,failures:e.failures}}function ke(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function G(r,e,t){let n;if(e.startsWith("http://")||e.startsWith("https://")){if(r){let i=new URL(r).origin,l=new URL(e).origin;if(i!==l)throw new Error(`Absolute URL "${e}" does not match baseUrl origin "${i}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=e}else{if(e.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=r?`${r.replace(/\/$/,"")}/${e.replace(/^\//,"")}`:e}if(!t||Object.keys(t).length===0)return n;let o=new URLSearchParams;for(let[i,l]of Object.entries(t))o.set(i,String(l));return`${n}?${o.toString()}`}function Se(r){return r instanceof ReadableStream||r!==null&&typeof r=="object"&&typeof r.pipe=="function"}function qe(r,e){let t={...e.headers,...r.headers},n=r.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!Se(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function Ae(r){if(r!=null)return r instanceof FormData||r instanceof Blob||r instanceof ArrayBuffer||typeof r=="string"||r instanceof ReadableStream||typeof r.pipe=="function"?r:JSON.stringify(r)}function De(r,e){if(e)return e;try{return new URL(r).origin}catch{return r}}async function Re(r,e,t){let n=e.requestId??ke(),o=e.method??"GET",i=Date.now(),l=re(e.retry??t.retry),s=e.debug??t.debug??!1,u=e.throwOnError??t.throwOnError??!0,a={...t.query,...e.query},H=qe(e,t),g=e.timeout??t.timeout,T=e.auth??t.auth;T&&q(H,a,T);let m=G(t.baseUrl??"",r,Object.keys(a).length>0?a:void 0);(e.proxy??t.proxy)&&s&&console.warn("[hurl] proxy option is not yet implemented. Node 18: npm install undici@6, use ProxyAgent + setGlobalDispatcher. Node 20: use ProxyAgent + setGlobalDispatcher from undici. Node 22.3+: use EnvHttpProxyAgent + setGlobalDispatcher from undici. Node 24+: set NODE_USE_ENV_PROXY=1 with HTTP_PROXY env var. See README for details.");let c=e.cache??t.cache,p=!!c&&!c.bypass&&o==="GET";if(p){let f=L(m,c),y=oe(f);if(y)return s&&F(y),y}let I=e.deduplicate??t.deduplicate??!1;if(I&&o==="GET"){let f=ae(m);if(f)return f}let R=e.circuitBreaker??t.circuitBreaker,w=R?De(m,R.key):"";if(R&&pe(w,R)==="OPEN"){if(s&&console.warn(`[hurl] circuit breaker OPEN for "${w}", fast-failing`),R.fallback){let y=Date.now();return{data:R.fallback(),status:0,statusText:"Circuit Open",headers:{},requestId:n,timing:{start:i,end:y,duration:y-i},fromCache:!1}}throw Y(w,n)}s&&ce(m,{...e,method:o});let x=async f=>{let y=null,K=!1,k=new AbortController,O=e.signal,S=null;O&&(O.aborted?k.abort(O.reason):(S=()=>k.abort(O.reason),O.addEventListener("abort",S,{once:!0}))),g&&(y=setTimeout(()=>{K=!0,k.abort()},g));try{let h=Ae(e.body),b=e.onUploadProgress??t.onUploadProgress;h!==void 0&&b&&(e.body instanceof FormData?s&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):h=Z(h,b));let P=await fetch(m,{method:o,headers:H,body:h,signal:k.signal,redirect:e.followRedirects??!0?"follow":"manual"}),W=await Q(P,n,o,e.stream??!1,e.onDownloadProgress??t.onDownloadProgress);if(!P.ok&&u)throw j({status:P.status,statusText:P.statusText,data:W,headers:N(P.headers),requestId:n,retries:f});let U=ee(W,P,n,i);return p&&c&&se(L(m,c),U,c),s&&F(U),U}catch(h){let b;if(h instanceof d?b=h:h.name==="AbortError"||h.code==="ABORT_ERR"?b=K?X(g,n):J(n):b=z(h.message,n),b.retries=f,l&&te(b,l,f))return s&&console.log(`[hurl] retrying (${f+1}/${l.count})...`),await ne(l,f),x(f+1);throw s&&fe(b),b}finally{y&&clearTimeout(y),S&&O&&O.removeEventListener("abort",S)}},C=R?x(0).then(f=>(de(w),f),f=>{throw f instanceof d&&f.type!=="ABORT_ERROR"&&f.type!=="CIRCUIT_OPEN"&&ge(w,R),f}):x(0);return I&&o==="GET"&&le(m,C),C}function D(){let r=[];return{use(e){return r.push(e),()=>{let t=r.indexOf(e);t!==-1&&r.splice(t,1)}},clear(){r.length=0},getAll(){return[...r]}}}async function ye(r,e,t){let n={url:e,options:t};for(let o of r)n=await o(n.url,n.options);return n}async function he(r,e){let t=e;for(let n of r)t=await n(t);return t}async function be(r,e){let t=e;for(let n of r)t instanceof d&&(t=await n(t));return t}function ve(r){let e={event:"message",id:"",data:""},t=r.split(`
|
|
2
|
+
`);for(let n of t)if(n.startsWith("data:")){let o=n.slice(5).trim();if(o==="[DONE]")return e._done=!0,e;e.data=(e.data?e.data+`
|
|
3
|
+
`:"")+o}else if(n.startsWith("event:"))e.event=n.slice(6).trim();else if(n.startsWith("id:"))e.id=n.slice(3).trim();else if(n.startsWith("retry:")){let o=parseInt(n.slice(6).trim(),10);isNaN(o)||(e.retry=o)}return e}function Ee(r,e,t){let n=new AbortController;e.signal&&(e.signal.aborted?n.abort():e.signal.addEventListener("abort",()=>n.abort(),{once:!0}));let o={...t.headers,...e.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},i={...t.query,...e.query},l=e.auth??t.auth;l&&q(o,i,l),e.body!==void 0&&e.body!==null&&(o["Content-Type"]=o["Content-Type"]??"application/json");let s=G(t.baseUrl??"",r,Object.keys(i).length>0?i:void 0),u=e.method??"GET",a=e.body===void 0||e.body===null?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body);async function H(){let g;try{g=await fetch(s,{method:u,headers:o,body:a,signal:n.signal})}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)));return}if(!g.ok){e.onError?.(new Error(`HTTP ${g.status}: ${g.statusText}`));return}if(!g.body){e.onError?.(new Error("Response body is empty"));return}e.onOpen?.();let T=g.body.getReader(),m=new TextDecoder,c="";try{for(;;){let{done:p,value:I}=await T.read();if(p)break;c+=m.decode(I,{stream:!0});let R=c.split(`
|
|
4
|
+
|
|
5
|
+
`);c=R.pop()??"";for(let w of R){let x=w.trim();if(!x)continue;let C=ve(x);if(C._done){e.onDone?.(),n.abort();return}C.data!==void 0&&e.onMessage(C)}}}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)))}finally{T.releaseLock()}e.onDone?.()}return H(),{close(){n.abort()}}}function v(r={}){let e={...r},t=D(),n=D(),o=D();async function i(s,u={}){let a=s,H=u,g=t.getAll(),T=n.getAll(),m=o.getAll();if(g.length>0){let c=await ye(g,s,u);a=c.url,H=c.options}try{let c=await Re(a,H,e);return T.length>0?await he(T,c):c}catch(c){if(c instanceof d&&m.length>0){let p=await be(m,c);if(!(p instanceof d))return p;throw p}throw c}}return{request:i,get(s,u){return i(s,{...u,method:"GET"})},post(s,u,a){return i(s,{...a,method:"POST",body:u})},put(s,u,a){return i(s,{...a,method:"PUT",body:u})},patch(s,u,a){return i(s,{...a,method:"PATCH",body:u})},delete(s,u){return i(s,{...u,method:"DELETE"})},head(s,u){return i(s,{...u,method:"HEAD"})},options(s,u){return i(s,{...u,method:"OPTIONS"})},sse(s,u){return Ee(s,u,e)},all(s){return Promise.all(s)},defaults:{set(s){e={...e,...s}},get(){return{...e}},reset(){e={...r}}},interceptors:{request:{use:t.use.bind(t),clear:t.clear.bind(t)},response:{use:n.use.bind(n),clear:n.clear.bind(n)},error:{use:o.use.bind(o),clear:o.clear.bind(o)}},create(s){return v({...e,...s})},extend(s){let u=v({...e,...s});return t.getAll().forEach(a=>u.interceptors.request.use(a)),n.getAll().forEach(a=>u.interceptors.response.use(a)),o.getAll().forEach(a=>u.interceptors.error.use(a)),u}}}var Ue=v(),Be=Ue;0&&(module.exports={HurlError,clearCache,createInstance,getCircuitStats,invalidateCache});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=class extends Error{constructor(e){super(e.message),this.name="HurlError",this.type=e.type,this.status=e.status,this.statusText=e.statusText,this.data=e.data,this.headers=e.headers,this.requestId=e.requestId,this.retries=e.retries??0}};function W(r){return new d({message:`HTTP ${r.status}: ${r.statusText}`,type:"HTTP_ERROR",...r})}function j(r,e){return new d({message:r,type:"NETWORK_ERROR",requestId:e})}function z(r,e){return new d({message:`Request timed out after ${r}ms`,type:"TIMEOUT_ERROR",requestId:e})}function X(r){return new d({message:"Request was aborted",type:"ABORT_ERROR",requestId:r})}function U(r,e){return new d({message:`Failed to parse response: ${r}`,type:"PARSE_ERROR",requestId:e})}function J(r,e){return new d({message:`Circuit breaker is open for "${r}"`,type:"CIRCUIT_OPEN",requestId:e})}async function Y(r,e){let t=r.body?.getReader(),n=parseInt(r.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let s=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;s.push(a),i+=a.byteLength,e({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let l=new Uint8Array(i),o=0;for(let u of s)l.set(u,o),o+=u.byteLength;return l.buffer}function V(r,e){let t=0;typeof r=="string"?t=new TextEncoder().encode(r).byteLength:r instanceof ArrayBuffer?t=r.byteLength:r instanceof Blob&&(t=r.size);let n=0;return(r instanceof ReadableStream?r:new Response(r).body).pipeThrough(new TransformStream({transform(i,l){n+=i.byteLength,e({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),l.enqueue(i)}}))}function B(r){let e={};return r.forEach((t,n)=>{e[n]=t}),e}async function Z(r,e,t,n,s){if(t==="HEAD"||r.status===204||r.headers.get("content-length")==="0")return null;if(n)return r.body;let i=r.headers.get("content-type")??"",l=i.includes("application/octet-stream")||i.includes("image/")||i.includes("video/")||i.includes("audio/");if(s&&l)try{return await Y(r,s)}catch(o){throw U(o.message,e)}try{return i.includes("application/json")?await r.json():i.includes("text/")?await r.text():l?await r.arrayBuffer():await r.text()}catch(o){throw U(o.message,e)}}function Q(r,e,t,n){let s=Date.now();return{data:r,status:e.status,statusText:e.statusText,headers:B(e.headers),requestId:t,timing:{start:n,end:s,duration:s-n},fromCache:!1}}function ye(r){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(r).toString("base64"):btoa(encodeURIComponent(r).replace(/%([0-9A-F]{2})/g,(e,t)=>String.fromCharCode(parseInt(t,16))))}function q(r,e,t){if(t.type==="bearer"&&(r.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=ye(`${t.username}:${t.password}`);r.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?e[t.key]=t.value:r[t.key]=t.value)}function ee(r){return r==null?null:typeof r=="number"?{count:r,delay:300,backoff:"exponential"}:r}function re(r,e,t){return t>=e.count||r.type==="ABORT_ERROR"?!1:e.on&&r.status?e.on.includes(r.status):!!(r.type==="NETWORK_ERROR"||r.type==="TIMEOUT_ERROR"||r.status&&r.status>=500)}async function te(r,e){let t=r.delay??300,n=r.backoff==="exponential"?t*Math.pow(2,e):t*(e+1);await new Promise(s=>setTimeout(s,n))}var E=new Map,he=1e3;function _(r,e){return e?.key??r}function ne(r){let e=E.get(r);if(!e)return null;if(Date.now()>e.expiresAt)return E.delete(r),null;let t=e.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...e.response,data:t,fromCache:!0}}function oe(r,e,t){if(E.size>=he&&!E.has(r)){let n=E.keys().next().value;n!==void 0&&E.delete(n)}E.set(r,{response:e,expiresAt:Date.now()+t.ttl})}function be(r){E.delete(r)}function Ee(){E.clear()}var N=new Map;function se(r){return N.get(r)??null}function ie(r,e){N.set(r,e),e.finally(()=>N.delete(r))}function ue(r,e){console.group(`[hurl] \u2192 ${e.method??"GET"} ${r}`),e.headers&&Object.keys(e.headers).length>0&&console.log("headers:",e.headers),e.query&&console.log("query:",e.query),e.body&&console.log("body:",e.body),e.timeout&&console.log("timeout:",e.timeout),e.retry&&console.log("retry:",e.retry),console.groupEnd()}function L(r){let e=r.status>=400?"\u{1F534}":r.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${e} ${r.status} ${r.statusText} (${r.timing.duration}ms)`),console.log("requestId:",r.requestId),r.fromCache&&console.log("served from cache"),console.log("headers:",r.headers),console.log("data:",r.data),console.groupEnd()}function ae(r){console.group("[hurl] \u{1F534} Error"),console.error(r),console.groupEnd()}var $=new Map;function A(r){return $.has(r)||$.set(r,{state:"CLOSED",failures:0,openedAt:0}),$.get(r)}function le(r,e){let t=A(r);return t.state==="OPEN"?Date.now()-t.openedAt>=e.cooldown?(t.state="HALF_OPEN","HALF_OPEN"):"OPEN":t.state}function ce(r){let e=A(r);e.state="CLOSED",e.failures=0}function fe(r,e){let t=A(r);t.failures+=1,t.failures>=e.threshold&&(t.state="OPEN",t.openedAt=Date.now())}function Te(r){let e=A(r);return{state:e.state,failures:e.failures}}function He(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function F(r,e,t){let n;if(e.startsWith("http://")||e.startsWith("https://")){if(r){let i=new URL(r).origin,l=new URL(e).origin;if(i!==l)throw new Error(`Absolute URL "${e}" does not match baseUrl origin "${i}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=e}else{if(e.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=r?`${r.replace(/\/$/,"")}/${e.replace(/^\//,"")}`:e}if(!t||Object.keys(t).length===0)return n;let s=new URLSearchParams;for(let[i,l]of Object.entries(t))s.set(i,String(l));return`${n}?${s.toString()}`}function we(r){return r instanceof ReadableStream||r!==null&&typeof r=="object"&&typeof r.pipe=="function"}function Oe(r,e){let t={...e.headers,...r.headers},n=r.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!we(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function xe(r){if(r!=null)return r instanceof FormData||r instanceof Blob||r instanceof ArrayBuffer||typeof r=="string"||r instanceof ReadableStream||typeof r.pipe=="function"?r:JSON.stringify(r)}function Ce(r,e){if(e)return e;try{return new URL(r).origin}catch{return r}}async function pe(r,e,t){let n=e.requestId??He(),s=e.method??"GET",i=Date.now(),l=ee(e.retry??t.retry),o=e.debug??t.debug??!1,u=e.throwOnError??t.throwOnError??!0,a={...t.query,...e.query},H=Oe(e,t),g=e.timeout??t.timeout,T=e.auth??t.auth;T&&q(H,a,T);let m=F(t.baseUrl??"",r,Object.keys(a).length>0?a:void 0);(e.proxy??t.proxy)&&o&&console.warn("[hurl] proxy option is not yet implemented. Node 18: npm install undici@6, use ProxyAgent + setGlobalDispatcher. Node 20: use ProxyAgent + setGlobalDispatcher from undici. Node 22.3+: use EnvHttpProxyAgent + setGlobalDispatcher from undici. Node 24+: set NODE_USE_ENV_PROXY=1 with HTTP_PROXY env var. See README for details.");let c=e.cache??t.cache,p=!!c&&!c.bypass&&s==="GET";if(p){let f=_(m,c),y=ne(f);if(y)return o&&L(y),y}let I=e.deduplicate??t.deduplicate??!1;if(I&&s==="GET"){let f=se(m);if(f)return f}let R=e.circuitBreaker??t.circuitBreaker,w=R?Ce(m,R.key):"";if(R&&le(w,R)==="OPEN"){if(o&&console.warn(`[hurl] circuit breaker OPEN for "${w}", fast-failing`),R.fallback){let y=Date.now();return{data:R.fallback(),status:0,statusText:"Circuit Open",headers:{},requestId:n,timing:{start:i,end:y,duration:y-i},fromCache:!1}}throw J(w,n)}o&&ue(m,{...e,method:s});let x=async f=>{let y=null,G=!1,k=new AbortController,O=e.signal,S=null;O&&(O.aborted?k.abort(O.reason):(S=()=>k.abort(O.reason),O.addEventListener("abort",S,{once:!0}))),g&&(y=setTimeout(()=>{G=!0,k.abort()},g));try{let h=xe(e.body),b=e.onUploadProgress??t.onUploadProgress;h!==void 0&&b&&(e.body instanceof FormData?o&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):h=V(h,b));let P=await fetch(m,{method:s,headers:H,body:h,signal:k.signal,redirect:e.followRedirects??!0?"follow":"manual"}),K=await Z(P,n,s,e.stream??!1,e.onDownloadProgress??t.onDownloadProgress);if(!P.ok&&u)throw W({status:P.status,statusText:P.statusText,data:K,headers:B(P.headers),requestId:n,retries:f});let v=Q(K,P,n,i);return p&&c&&oe(_(m,c),v,c),o&&L(v),v}catch(h){let b;if(h instanceof d?b=h:h.name==="AbortError"||h.code==="ABORT_ERR"?b=G?z(g,n):X(n):b=j(h.message,n),b.retries=f,l&&re(b,l,f))return o&&console.log(`[hurl] retrying (${f+1}/${l.count})...`),await te(l,f),x(f+1);throw o&&ae(b),b}finally{y&&clearTimeout(y),S&&O&&O.removeEventListener("abort",S)}},C=R?x(0).then(f=>(ce(w),f),f=>{throw f instanceof d&&f.type!=="ABORT_ERROR"&&f.type!=="CIRCUIT_OPEN"&&fe(w,R),f}):x(0);return I&&s==="GET"&&ie(m,C),C}function D(){let r=[];return{use(e){return r.push(e),()=>{let t=r.indexOf(e);t!==-1&&r.splice(t,1)}},clear(){r.length=0},getAll(){return[...r]}}}async function de(r,e,t){let n={url:e,options:t};for(let s of r)n=await s(n.url,n.options);return n}async function ge(r,e){let t=e;for(let n of r)t=await n(t);return t}async function me(r,e){let t=e;for(let n of r)t instanceof d&&(t=await n(t));return t}function Pe(r){let e={event:"message",id:"",data:""},t=r.split(`
|
|
2
|
+
`);for(let n of t)if(n.startsWith("data:")){let s=n.slice(5).trim();if(s==="[DONE]")return e._done=!0,e;e.data=(e.data?e.data+`
|
|
3
|
+
`:"")+s}else if(n.startsWith("event:"))e.event=n.slice(6).trim();else if(n.startsWith("id:"))e.id=n.slice(3).trim();else if(n.startsWith("retry:")){let s=parseInt(n.slice(6).trim(),10);isNaN(s)||(e.retry=s)}return e}function Re(r,e,t){let n=new AbortController;e.signal&&(e.signal.aborted?n.abort():e.signal.addEventListener("abort",()=>n.abort(),{once:!0}));let s={...t.headers,...e.headers,Accept:"text/event-stream","Cache-Control":"no-cache"},i={...t.query,...e.query},l=e.auth??t.auth;l&&q(s,i,l),e.body!==void 0&&e.body!==null&&(s["Content-Type"]=s["Content-Type"]??"application/json");let o=F(t.baseUrl??"",r,Object.keys(i).length>0?i:void 0),u=e.method??"GET",a=e.body===void 0||e.body===null?void 0:typeof e.body=="string"?e.body:JSON.stringify(e.body);async function H(){let g;try{g=await fetch(o,{method:u,headers:s,body:a,signal:n.signal})}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)));return}if(!g.ok){e.onError?.(new Error(`HTTP ${g.status}: ${g.statusText}`));return}if(!g.body){e.onError?.(new Error("Response body is empty"));return}e.onOpen?.();let T=g.body.getReader(),m=new TextDecoder,c="";try{for(;;){let{done:p,value:I}=await T.read();if(p)break;c+=m.decode(I,{stream:!0});let R=c.split(`
|
|
4
|
+
|
|
5
|
+
`);c=R.pop()??"";for(let w of R){let x=w.trim();if(!x)continue;let C=Pe(x);if(C._done){e.onDone?.(),n.abort();return}C.data!==void 0&&e.onMessage(C)}}}catch(p){if(p.name==="AbortError")return;e.onError?.(p instanceof Error?p:new Error(String(p)))}finally{T.releaseLock()}e.onDone?.()}return H(),{close(){n.abort()}}}function M(r={}){let e={...r},t=D(),n=D(),s=D();async function i(o,u={}){let a=o,H=u,g=t.getAll(),T=n.getAll(),m=s.getAll();if(g.length>0){let c=await de(g,o,u);a=c.url,H=c.options}try{let c=await pe(a,H,e);return T.length>0?await ge(T,c):c}catch(c){if(c instanceof d&&m.length>0){let p=await me(m,c);if(!(p instanceof d))return p;throw p}throw c}}return{request:i,get(o,u){return i(o,{...u,method:"GET"})},post(o,u,a){return i(o,{...a,method:"POST",body:u})},put(o,u,a){return i(o,{...a,method:"PUT",body:u})},patch(o,u,a){return i(o,{...a,method:"PATCH",body:u})},delete(o,u){return i(o,{...u,method:"DELETE"})},head(o,u){return i(o,{...u,method:"HEAD"})},options(o,u){return i(o,{...u,method:"OPTIONS"})},sse(o,u){return Re(o,u,e)},all(o){return Promise.all(o)},defaults:{set(o){e={...e,...o}},get(){return{...e}},reset(){e={...r}}},interceptors:{request:{use:t.use.bind(t),clear:t.clear.bind(t)},response:{use:n.use.bind(n),clear:n.clear.bind(n)},error:{use:s.use.bind(s),clear:s.clear.bind(s)}},create(o){return M({...e,...o})},extend(o){let u=M({...e,...o});return t.getAll().forEach(a=>u.interceptors.request.use(a)),n.getAll().forEach(a=>u.interceptors.response.use(a)),s.getAll().forEach(a=>u.interceptors.error.use(a)),u}}}var Ie=M(),Ir=Ie;export{d as HurlError,Ee as clearCache,M as createInstance,Ir as default,Te as getCircuitStats,be as invalidateCache};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firekid/hurl",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Zero-dependency HTTP client for Node.js and edge runtimes. Built on fetch. The modern replacement for axios, request, got, node-fetch, and ky. Works on Cloudflare Workers, Vercel Edge, Deno, and Bun.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
"typescript": "^5.0.0",
|
|
107
107
|
"vitest": "^1.0.0"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|