@firekid/hurl 1.0.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 hurl contributors
3
+ Copyright (c) 2026 hurl contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,26 +1,73 @@
1
- # hurl
1
+ # @firekid/hurl
2
2
 
3
- A modern HTTP client for Node.js and edge runtimes. Zero dependencies. Full TypeScript support. Built to replace `request` and `axios` with a smaller, faster, and more capable alternative.
3
+ [![npm version](https://img.shields.io/npm/v/@firekid/hurl?style=flat-square&logo=npm&logoColor=white&color=CB3837)](https://npmjs.com/package/@firekid/hurl)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@firekid/hurl?style=flat-square&logo=npm&logoColor=white&color=CB3837)](https://npmjs.com/package/@firekid/hurl)
5
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@firekid/hurl?style=flat-square&logo=webpack&logoColor=white&color=2563EB)](https://bundlephobia.com/package/@firekid/hurl)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22C55E?style=flat-square)](./LICENSE)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/firekid-is-him/hurl/ci.yml?style=flat-square&logo=githubactions&logoColor=white&label=CI)](https://github.com/firekid-is-him/hurl/actions)
9
+ [![GitHub stars](https://img.shields.io/github/stars/firekid-is-him/hurl?style=flat-square&logo=github&logoColor=white&color=FACC15)](https://github.com/firekid-is-him/hurl/stargazers)
10
+ [![Website](https://img.shields.io/badge/website-hurl.firekidofficial.name.ng-black?style=flat-square&logo=googlechrome&logoColor=white)](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
- ## GitHub
18
+ ---
10
19
 
11
- https://github.com/Firekid-is-him/hurl
20
+ ## Why hurl?
12
21
 
13
- ## Purpose
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
- `hurl` solves the problems that `request` left behind when it was deprecated and that `axios` never fully addressed: no edge runtime support, a 35KB bundle, no built-in retry logic, no request deduplication, and no upload progress tracking. `hurl` ships all of these in under 3KB with zero runtime dependencies.
24
+ ```ts
25
+ import hurl from '@firekid/hurl'
16
26
 
17
- ## Core Concepts
27
+ // Retry automatically on failure
28
+ const res = await hurl.get('https://api.example.com/users', { retry: 3 })
18
29
 
19
- Every method on `hurl` returns a `HurlResponse<T>` object. The response always includes the parsed data, status code, headers, a unique request ID, timing information, and a flag indicating whether the response was served from cache.
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
- Defaults are set globally using `hurl.defaults.set()` and apply to every request made on that instance. Isolated instances with their own defaults can be created using `hurl.create()`.
37
+ // Parallel requests
38
+ const [users, posts] = await hurl.all([
39
+ hurl.get('/users'),
40
+ hurl.get('/posts'),
41
+ ])
42
+ ```
22
43
 
23
- Interceptors run in the order they were registered and can be async. A request interceptor receives the URL and options before the request is sent. A response interceptor receives the full response object. An error interceptor receives a `HurlError` and can either return a modified error or resolve it into a response.
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 &nbsp; ⚠️ Partial / via plugin &nbsp; ❌ 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,60 +125,33 @@ hurl.defaults.get()
72
125
  hurl.defaults.reset()
73
126
  ```
74
127
 
75
- ## Request Options
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
- debug?: boolean
97
- requestId?: string
98
- deduplicate?: boolean
99
- }
100
- ```
128
+ ---
101
129
 
102
130
  ## Authentication
103
131
 
104
132
  ```ts
105
- hurl.defaults.set({
106
- auth: { type: 'bearer', token: 'my-token' }
107
- })
133
+ // Bearer token
134
+ hurl.defaults.set({ auth: { type: 'bearer', token: 'my-token' } })
108
135
 
109
- hurl.defaults.set({
110
- auth: { type: 'basic', username: 'admin', password: 'secret' }
111
- })
136
+ // Basic auth
137
+ hurl.defaults.set({ auth: { type: 'basic', username: 'admin', password: 'secret' } })
112
138
 
113
- hurl.defaults.set({
114
- auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' }
115
- })
139
+ // API key (header)
140
+ hurl.defaults.set({ auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' } })
116
141
 
117
- hurl.defaults.set({
118
- auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' }
119
- })
142
+ // API key (query param)
143
+ hurl.defaults.set({ auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' } })
120
144
  ```
121
145
 
122
- ## Retry
146
+ ---
147
+
148
+ ## Retry & Backoff
123
149
 
124
150
  ```ts
125
- await hurl.get('/users', {
126
- retry: 3
127
- })
151
+ // Simple — retry 3 times with exponential backoff
152
+ await hurl.get('/users', { retry: 3 })
128
153
 
154
+ // Full config
129
155
  await hurl.get('/users', {
130
156
  retry: {
131
157
  count: 3,
@@ -136,9 +162,11 @@ await hurl.get('/users', {
136
162
  })
137
163
  ```
138
164
 
139
- `retry` accepts a number (shorthand for count with exponential backoff) or a full `RetryConfig` object. Retries are not triggered for abort errors. If no `on` array is provided, retries fire on network errors, timeout errors, and any 5xx status.
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
+ ---
140
168
 
141
- ## Timeout and Abort
169
+ ## Timeout & Abort
142
170
 
143
171
  ```ts
144
172
  await hurl.get('/users', { timeout: 5000 })
@@ -148,9 +176,12 @@ setTimeout(() => controller.abort(), 3000)
148
176
  await hurl.get('/users', { signal: controller.signal })
149
177
  ```
150
178
 
179
+ ---
180
+
151
181
  ## Interceptors
152
182
 
153
183
  ```ts
184
+ // Request interceptor
154
185
  const remove = hurl.interceptors.request.use((url, options) => {
155
186
  return {
156
187
  url,
@@ -160,65 +191,51 @@ const remove = hurl.interceptors.request.use((url, options) => {
160
191
  },
161
192
  }
162
193
  })
194
+ remove() // unregister
163
195
 
164
- remove()
165
-
196
+ // Response interceptor
166
197
  hurl.interceptors.response.use((response) => {
167
198
  console.log(response.status, response.timing.duration)
168
199
  return response
169
200
  })
170
201
 
202
+ // Error interceptor
171
203
  hurl.interceptors.error.use((error) => {
172
204
  if (error.status === 401) redirectToLogin()
173
205
  return error
174
206
  })
175
207
 
208
+ // Clear all
176
209
  hurl.interceptors.request.clear()
177
210
  hurl.interceptors.response.clear()
178
211
  hurl.interceptors.error.clear()
179
212
  ```
180
213
 
181
- ## File Upload with Progress
214
+ ---
182
215
 
183
- ```ts
184
- const form = new FormData()
185
- form.append('file', file)
186
-
187
- await hurl.post('/upload', form, {
188
- onUploadProgress: ({ loaded, total, percent }) => {
189
- console.log(`${percent}%`)
190
- }
191
- })
192
- ```
216
+ ## Caching
193
217
 
194
- ## Download Progress
218
+ Caching applies to GET requests only. Responses are stored in memory with a TTL in milliseconds.
195
219
 
196
220
  ```ts
197
- await hurl.get('/large-file', {
198
- onDownloadProgress: ({ loaded, total, percent }) => {
199
- console.log(`${percent}%`)
200
- }
201
- })
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 } })
202
224
  ```
203
225
 
204
- ## Caching
205
-
206
- Caching only applies to GET requests. Responses are stored in memory with a TTL in milliseconds.
207
-
208
226
  ```ts
209
- await hurl.get('/users', {
210
- cache: { ttl: 60000 }
211
- })
227
+ import { clearCache, invalidateCache } from '@firekid/hurl'
212
228
 
213
- await hurl.get('/users', {
214
- cache: { ttl: 60000, key: 'all-users' }
215
- })
229
+ // Clear the entire cache
230
+ clearCache()
216
231
 
217
- await hurl.get('/users', {
218
- cache: { ttl: 60000, bypass: true }
219
- })
232
+ // Invalidate a single entry by URL or custom key
233
+ invalidateCache('https://api.example.com/users')
234
+ invalidateCache('all-users') // if you used a custom cache key
220
235
  ```
221
236
 
237
+ ---
238
+
222
239
  ## Request Deduplication
223
240
 
224
241
  When `deduplicate` is true and the same GET URL is called multiple times simultaneously, only one network request is made.
@@ -228,23 +245,68 @@ const [a, b] = await Promise.all([
228
245
  hurl.get('/users', { deduplicate: true }),
229
246
  hurl.get('/users', { deduplicate: true }),
230
247
  ])
248
+ // only one network request fired
231
249
  ```
232
250
 
233
- ## Proxy
251
+ ---
252
+
253
+ ## Upload & Download Progress
234
254
 
235
255
  ```ts
236
- await hurl.get('/users', {
237
- proxy: { url: 'http://proxy.example.com:8080' }
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
+ }
238
264
  })
239
265
 
240
- await hurl.get('/users', {
241
- proxy: {
242
- url: 'socks5://proxy.example.com:1080',
243
- auth: { username: 'user', password: 'pass' }
266
+ // Download
267
+ await hurl.get('/large-file', {
268
+ onDownloadProgress: ({ loaded, total, percent }) => {
269
+ console.log(`Downloading: ${percent}%`)
244
270
  }
245
271
  })
246
272
  ```
247
273
 
274
+ ---
275
+
276
+ ## Proxy
277
+
278
+ Native fetch does not support programmatic proxy configuration out of the box. Proxy support depends on your Node.js version:
279
+
280
+ **Node.js 18** — install `undici@6` (v7 dropped Node 18 support), use `ProxyAgent`:
281
+ ```ts
282
+ // npm install undici@6
283
+ import { ProxyAgent, setGlobalDispatcher } from 'undici'
284
+ setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))
285
+ ```
286
+
287
+ **Node.js 20** — `undici` is bundled with `ProxyAgent` support:
288
+ ```ts
289
+ import { ProxyAgent, setGlobalDispatcher } from 'undici'
290
+ setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))
291
+ ```
292
+
293
+ **Node.js 22.3+** — supports `EnvHttpProxyAgent` which reads `HTTP_PROXY`/`HTTPS_PROXY` env vars automatically:
294
+ ```ts
295
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'
296
+ setGlobalDispatcher(new EnvHttpProxyAgent())
297
+ // now set HTTP_PROXY=http://proxy.example.com:8080 in your env
298
+ ```
299
+
300
+ **Node.js 24+** — native fetch respects env vars when `NODE_USE_ENV_PROXY=1` is set:
301
+ ```bash
302
+ NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 node app.js
303
+ ```
304
+
305
+ The `proxy` option in `HurlRequestOptions` is reserved for a future release where this will be handled automatically.
306
+
307
+ ---
308
+ ---
309
+
248
310
  ## Parallel Requests
249
311
 
250
312
  ```ts
@@ -254,6 +316,8 @@ const [users, posts] = await hurl.all([
254
316
  ])
255
317
  ```
256
318
 
319
+ ---
320
+
257
321
  ## Isolated Instances
258
322
 
259
323
  ```ts
@@ -266,22 +330,26 @@ const api = hurl.create({
266
330
 
267
331
  await api.get('/users')
268
332
 
333
+ // Extend with overrides
269
334
  const adminApi = api.extend({
270
335
  headers: { 'x-role': 'admin' }
271
336
  })
272
337
  ```
273
338
 
274
- ## Debug Mode
339
+ ---
275
340
 
276
- 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.
341
+ ## Error Handling
277
342
 
278
- ```ts
279
- await hurl.get('/users', { debug: true })
280
- ```
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.
281
344
 
282
- ## Error Handling
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.
283
346
 
284
- `hurl` throws a `HurlError` on HTTP errors (4xx, 5xx), network failures, timeouts, aborts, and parse failures. It never resolves silently on bad status codes.
347
+ ```ts
348
+ const res = await hurl.get('/users', { throwOnError: false })
349
+ if (res.status === 404) {
350
+ console.log('not found')
351
+ }
352
+ ```
285
353
 
286
354
  ```ts
287
355
  import hurl, { HurlError } from '@firekid/hurl'
@@ -301,18 +369,32 @@ try {
301
369
  }
302
370
  ```
303
371
 
372
+ ---
373
+
304
374
  ## TypeScript
305
375
 
306
376
  ```ts
307
377
  type User = { id: number; name: string }
308
378
 
309
379
  const res = await hurl.get<User[]>('/users')
310
- res.data
380
+ res.data // User[]
311
381
 
312
382
  const created = await hurl.post<User>('/users', { name: 'John' })
313
- 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 })
314
394
  ```
315
395
 
396
+ ---
397
+
316
398
  ## Response Shape
317
399
 
318
400
  ```ts
@@ -331,79 +413,96 @@ type HurlResponse<T> = {
331
413
  }
332
414
  ```
333
415
 
334
- ## Environment Support
335
-
336
- `hurl` runs anywhere the Fetch API is available.
337
-
338
- - Node.js 18 and above
339
- - Cloudflare Workers
340
- - Vercel Edge Functions
341
- - Deno
342
- - Bun
343
-
344
- Exports both ESM (`import`) and CommonJS (`require`).
345
-
346
- ## API Reference
347
-
348
- ### hurl.get(url, options?)
349
- Sends a GET request. Returns `Promise<HurlResponse<T>>`.
416
+ ---
350
417
 
351
- ### hurl.post(url, body?, options?)
352
- Sends a POST request. Body is auto-serialized to JSON if it is a plain object. Returns `Promise<HurlResponse<T>>`.
353
-
354
- ### hurl.put(url, body?, options?)
355
- Sends a PUT request. Returns `Promise<HurlResponse<T>>`.
356
-
357
- ### hurl.patch(url, body?, options?)
358
- Sends a PATCH request. Returns `Promise<HurlResponse<T>>`.
418
+ ## Request Options
359
419
 
360
- ### hurl.delete(url, options?)
361
- Sends a DELETE request. Returns `Promise<HurlResponse<T>>`.
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
+ ```
362
442
 
363
- ### hurl.head(url, options?)
364
- Sends a HEAD request. Returns `Promise<HurlResponse<void>>`.
443
+ ---
365
444
 
366
- ### hurl.options(url, options?)
367
- Sends an OPTIONS request. Returns `Promise<HurlResponse<T>>`.
445
+ ## Environment Support
368
446
 
369
- ### hurl.request(url, options?)
370
- Sends a request with the method specified in options. Defaults to GET. Returns `Promise<HurlResponse<T>>`.
447
+ `@firekid/hurl` runs anywhere the Fetch API is available. No adapters, no polyfills needed.
371
448
 
372
- ### hurl.all(requests)
373
- Runs an array of requests in parallel. Returns a promise that resolves when all requests complete. Equivalent to `Promise.all`.
449
+ | Runtime | Support |
450
+ |---|:---:|
451
+ | Node.js 18+ | ✅ |
452
+ | Cloudflare Workers | ✅ |
453
+ | Vercel Edge Functions | ✅ |
454
+ | Deno | ✅ |
455
+ | Bun | ✅ |
374
456
 
375
- ### hurl.create(defaults?)
376
- Creates a new isolated instance with its own defaults, interceptors, and state. Does not share anything with the parent instance.
457
+ Exports both ESM (`import`) and CommonJS (`require`).
377
458
 
378
- ### hurl.extend(defaults?)
379
- Creates a new instance that inherits the current defaults and merges in the provided ones.
459
+ ---
380
460
 
381
- ### hurl.defaults.set(defaults)
382
- Sets global defaults for the current instance. Merged into every request.
461
+ ## Why Not Axios?
383
462
 
384
- ### hurl.defaults.get()
385
- Returns the current defaults object.
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.
386
464
 
387
- ### hurl.defaults.reset()
388
- Resets defaults to the values provided when the instance was created.
465
+ **got** dropped CommonJS in v12 — if your project uses `require()`, you're stuck on an old version.
389
466
 
390
- ### hurl.interceptors.request.use(fn)
391
- Registers a request interceptor. Returns a function that removes the interceptor when called.
467
+ **ky** is browser-first. No Node.js, no proxy, no streaming.
392
468
 
393
- ### hurl.interceptors.response.use(fn)
394
- Registers a response interceptor. Returns a function that removes the interceptor when called.
469
+ **node-fetch** is a polyfill. Node.js has had native fetch since v18. You don't need it anymore.
395
470
 
396
- ### hurl.interceptors.error.use(fn)
397
- Registers an error interceptor. Returns a function that removes the interceptor when called.
471
+ **request** has been deprecated since 2020.
398
472
 
399
- ### clearCache()
400
- Clears the entire in-memory response cache.
473
+ **`@firekid/hurl`** is built for how Node.js and the edge work today — native fetch, zero dependencies, everything included, works everywhere.
401
474
 
402
- ```ts
403
- import { clearCache } from '@firekid/hurl'
404
- clearCache()
405
- ```
475
+ ---
406
476
 
407
- ## License
477
+ ## API Reference
408
478
 
409
- MIT
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
+ [![HeavstalTech](https://github.com/HeavstalTech.png?size=32)](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 C=Object.defineProperty;var le=Object.getOwnPropertyDescriptor;var ce=Object.getOwnPropertyNames;var pe=Object.prototype.hasOwnProperty;var fe=(e,r)=>{for(var t in r)C(e,t,{get:r[t],enumerable:!0})},de=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of ce(r))!pe.call(e,s)&&s!==t&&C(e,s,{get:()=>r[s],enumerable:!(n=le(r,s))||n.enumerable});return e};var ge=e=>de(C({},"__esModule",{value:!0}),e);var xe={};fe(xe,{HurlError:()=>c,clearCache:()=>ee,createInstance:()=>q,default:()=>we});module.exports=ge(xe);var c=class extends Error{constructor(r){super(r.message),this.name="HurlError",this.type=r.type,this.status=r.status,this.statusText=r.statusText,this.data=r.data,this.headers=r.headers,this.requestId=r.requestId,this.retries=r.retries??0}};function F(e){return new c({message:`HTTP ${e.status}: ${e.statusText}`,type:"HTTP_ERROR",...e})}function M(e,r){return new c({message:e,type:"NETWORK_ERROR",requestId:r})}function K(e,r){return new c({message:`Request timed out after ${e}ms`,type:"TIMEOUT_ERROR",requestId:r})}function j(e){return new c({message:"Request was aborted",type:"ABORT_ERROR",requestId:e})}function k(e,r){return new c({message:`Failed to parse response: ${e}`,type:"PARSE_ERROR",requestId:r})}async function N(e,r){let t=e.body?.getReader(),n=parseInt(e.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let s=[],u=0;for(;;){let{done:i,value:p}=await t.read();if(i)break;s.push(p),u+=p.byteLength,r({loaded:u,total:n,percent:n>0?Math.round(u/n*100):0})}let a=new Uint8Array(u),o=0;for(let i of s)a.set(i,o),o+=i.byteLength;return a.buffer}function W(e,r){let t=0;typeof e=="string"?t=new TextEncoder().encode(e).byteLength:e instanceof ArrayBuffer?t=e.byteLength:e instanceof Blob&&(t=e.size);let n=0;return(e instanceof ReadableStream?e:new Response(e).body).pipeThrough(new TransformStream({transform(u,a){n+=u.byteLength,r({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),a.enqueue(u)}}))}function A(e){let r={};return e.forEach((t,n)=>{r[n]=t}),r}async function G(e,r,t,n,s){if(t==="HEAD"||e.status===204||e.headers.get("content-length")==="0")return null;if(n)return e.body;let u=e.headers.get("content-type")??"",a=u.includes("application/octet-stream")||u.includes("image/")||u.includes("video/")||u.includes("audio/");if(s&&a)try{return await N(e,s)}catch(o){throw k(o.message,r)}try{return u.includes("application/json")?await e.json():u.includes("text/")?await e.text():a?await e.arrayBuffer():await e.text()}catch(o){throw k(o.message,r)}}function z(e,r,t,n){let s=Date.now();return{data:e,status:r.status,statusText:r.statusText,headers:A(r.headers),requestId:t,timing:{start:n,end:s,duration:s-n},fromCache:!1}}function Re(e){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(e).toString("base64"):btoa(encodeURIComponent(e).replace(/%([0-9A-F]{2})/g,(r,t)=>String.fromCharCode(parseInt(t,16))))}function X(e,r,t){if(t.type==="bearer"&&(e.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=Re(`${t.username}:${t.password}`);e.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?r[t.key]=t.value:e[t.key]=t.value)}function Y(e){return e==null?null:typeof e=="number"?{count:e,delay:300,backoff:"exponential"}:e}function J(e,r,t){return t>=r.count||e.type==="ABORT_ERROR"?!1:r.on&&e.status?r.on.includes(e.status):!!(e.type==="NETWORK_ERROR"||e.type==="TIMEOUT_ERROR"||e.status&&e.status>=500)}async function Z(e,r){let t=e.delay??300,n=e.backoff==="exponential"?t*Math.pow(2,r):t*(r+1);await new Promise(s=>setTimeout(s,n))}var m=new Map,me=1e3;function U(e,r){return r?.key??e}function Q(e){let r=m.get(e);if(!r)return null;if(Date.now()>r.expiresAt)return m.delete(e),null;let t=r.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...r.response,data:t,fromCache:!0}}function V(e,r,t){if(m.size>=me&&!m.has(e)){let n=m.keys().next().value;n!==void 0&&m.delete(n)}m.set(e,{response:r,expiresAt:Date.now()+t.ttl})}function ee(){m.clear()}var D=new Map;function re(e){return D.get(e)??null}function te(e,r){D.set(e,r),r.finally(()=>D.delete(e))}function ne(e,r){console.group(`[hurl] \u2192 ${r.method??"GET"} ${e}`),r.headers&&Object.keys(r.headers).length>0&&console.log("headers:",r.headers),r.query&&console.log("query:",r.query),r.body&&console.log("body:",r.body),r.timeout&&console.log("timeout:",r.timeout),r.retry&&console.log("retry:",r.retry),console.groupEnd()}function B(e){let r=e.status>=400?"\u{1F534}":e.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${r} ${e.status} ${e.statusText} (${e.timing.duration}ms)`),console.log("requestId:",e.requestId),e.fromCache&&console.log("served from cache"),console.log("headers:",e.headers),console.log("data:",e.data),console.groupEnd()}function oe(e){console.group("[hurl] \u{1F534} Error"),console.error(e),console.groupEnd()}function he(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function ye(e,r,t){let n;if(r.startsWith("http://")||r.startsWith("https://")){if(e){let u=new URL(e).origin,a=new URL(r).origin;if(u!==a)throw new Error(`Absolute URL "${r}" does not match baseUrl origin "${u}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=r}else{if(r.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=e?`${e.replace(/\/$/,"")}/${r.replace(/^\//,"")}`:r}if(!t||Object.keys(t).length===0)return n;let s=new URLSearchParams;for(let[u,a]of Object.entries(t))s.set(u,String(a));return`${n}?${s.toString()}`}function Te(e){return e instanceof ReadableStream||e!==null&&typeof e=="object"&&typeof e.pipe=="function"}function He(e,r){let t={...r.headers,...e.headers},n=e.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!Te(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function be(e){if(e!=null)return e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||typeof e=="string"||e instanceof ReadableStream||typeof e.pipe=="function"?e:JSON.stringify(e)}async function se(e,r,t){let n=r.requestId??he(),s=r.method??"GET",u=Date.now(),a=Y(r.retry??t.retry),o=r.debug??t.debug??!1,i=r.throwOnError??t.throwOnError??!0,p={...t.query,...r.query},E=He(r,t),T=r.timeout??t.timeout,w=r.auth??t.auth;w&&X(E,p,w);let R=ye(t.baseUrl??"",e,Object.keys(p).length>0?p:void 0);(r.proxy??t.proxy)&&o&&console.warn("[hurl] proxy option is not supported with native fetch. Use HTTP_PROXY/HTTPS_PROXY env vars in Node.js.");let l=r.cache??t.cache,H=!!l&&!l.bypass&&s==="GET";if(H){let f=U(R,l),h=Q(f);if(h)return o&&B(h),h}let v=r.deduplicate??t.deduplicate??!1;if(v&&s==="GET"){let f=re(R);if(f)return f}o&&ne(R,{...r,method:s});let S=async f=>{let h=null,L=!1,x=new AbortController,y=r.signal,I=null;y&&(y.aborted?x.abort(y.reason):(I=()=>x.abort(y.reason),y.addEventListener("abort",I,{once:!0}))),T&&(h=setTimeout(()=>{L=!0,x.abort()},T));try{let d=be(r.body),g=r.onUploadProgress??t.onUploadProgress;d!==void 0&&g&&(r.body instanceof FormData?o&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):d=W(d,g));let b=await fetch(R,{method:s,headers:E,body:d,signal:x.signal,redirect:r.followRedirects??!0?"follow":"manual"}),_=await G(b,n,s,r.stream??!1,r.onDownloadProgress??t.onDownloadProgress);if(!b.ok&&i)throw F({status:b.status,statusText:b.statusText,data:_,headers:A(b.headers),requestId:n,retries:f});let P=z(_,b,n,u);return H&&l&&V(U(R,l),P,l),o&&B(P),P}catch(d){let g;if(d instanceof c?g=d:d.name==="AbortError"||d.code==="ABORT_ERR"?g=L?K(T,n):j(n):g=M(d.message,n),g.retries=f,a&&J(g,a,f))return o&&console.log(`[hurl] retrying (${f+1}/${a.count})...`),await Z(a,f),S(f+1);throw o&&oe(g),g}finally{h&&clearTimeout(h),I&&y&&y.removeEventListener("abort",I)}},$=S(0);return v&&s==="GET"&&te(R,$),$}function O(){let e=[];return{use(r){return e.push(r),()=>{let t=e.indexOf(r);t!==-1&&e.splice(t,1)}},clear(){e.length=0},getAll(){return[...e]}}}async function ue(e,r,t){let n={url:r,options:t};for(let s of e)n=await s(n.url,n.options);return n}async function ie(e,r){let t=r;for(let n of e)t=await n(t);return t}async function ae(e,r){let t=r;for(let n of e)t instanceof c&&(t=await n(t));return t}function q(e={}){let r={...e},t=O(),n=O(),s=O();async function u(o,i={}){let p=o,E=i,T=t.getAll(),w=n.getAll(),R=s.getAll();if(T.length>0){let l=await ue(T,o,i);p=l.url,E=l.options}try{let l=await se(p,E,r);return w.length>0?await ie(w,l):l}catch(l){if(l instanceof c&&R.length>0){let H=await ae(R,l);if(!(H instanceof c))return H;throw H}throw l}}return{request:u,get(o,i){return u(o,{...i,method:"GET"})},post(o,i,p){return u(o,{...p,method:"POST",body:i})},put(o,i,p){return u(o,{...p,method:"PUT",body:i})},patch(o,i,p){return u(o,{...p,method:"PATCH",body:i})},delete(o,i){return u(o,{...i,method:"DELETE"})},head(o,i){return u(o,{...i,method:"HEAD"})},options(o,i){return u(o,{...i,method:"OPTIONS"})},all(o){return Promise.all(o)},defaults:{set(o){r={...r,...o}},get(){return{...r}},reset(){r={...e}}},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 q({...r,...o})},extend(o){return q({...r,...o})}}}var Ee=q(),we=Ee;0&&(module.exports={HurlError,clearCache,createInstance});
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 p=class extends Error{constructor(r){super(r.message),this.name="HurlError",this.type=r.type,this.status=r.status,this.statusText=r.statusText,this.data=r.data,this.headers=r.headers,this.requestId=r.requestId,this.retries=r.retries??0}};function _(e){return new p({message:`HTTP ${e.status}: ${e.statusText}`,type:"HTTP_ERROR",...e})}function F(e,r){return new p({message:e,type:"NETWORK_ERROR",requestId:r})}function M(e,r){return new p({message:`Request timed out after ${e}ms`,type:"TIMEOUT_ERROR",requestId:r})}function K(e){return new p({message:"Request was aborted",type:"ABORT_ERROR",requestId:e})}function P(e,r){return new p({message:`Failed to parse response: ${e}`,type:"PARSE_ERROR",requestId:r})}async function j(e,r){let t=e.body?.getReader(),n=parseInt(e.headers.get("content-length")??"0",10);if(!t)return new ArrayBuffer(0);let u=[],s=0;for(;;){let{done:i,value:c}=await t.read();if(i)break;u.push(c),s+=c.byteLength,r({loaded:s,total:n,percent:n>0?Math.round(s/n*100):0})}let a=new Uint8Array(s),o=0;for(let i of u)a.set(i,o),o+=i.byteLength;return a.buffer}function N(e,r){let t=0;typeof e=="string"?t=new TextEncoder().encode(e).byteLength:e instanceof ArrayBuffer?t=e.byteLength:e instanceof Blob&&(t=e.size);let n=0;return(e instanceof ReadableStream?e:new Response(e).body).pipeThrough(new TransformStream({transform(s,a){n+=s.byteLength,r({loaded:n,total:t,percent:t>0?Math.round(n/t*100):0}),a.enqueue(s)}}))}function C(e){let r={};return e.forEach((t,n)=>{r[n]=t}),r}async function W(e,r,t,n,u){if(t==="HEAD"||e.status===204||e.headers.get("content-length")==="0")return null;if(n)return e.body;let s=e.headers.get("content-type")??"",a=s.includes("application/octet-stream")||s.includes("image/")||s.includes("video/")||s.includes("audio/");if(u&&a)try{return await j(e,u)}catch(o){throw P(o.message,r)}try{return s.includes("application/json")?await e.json():s.includes("text/")?await e.text():a?await e.arrayBuffer():await e.text()}catch(o){throw P(o.message,r)}}function G(e,r,t,n){let u=Date.now();return{data:e,status:r.status,statusText:r.statusText,headers:C(r.headers),requestId:t,timing:{start:n,end:u,duration:u-n},fromCache:!1}}function ie(e){return typeof globalThis<"u"&&globalThis.Buffer?globalThis.Buffer.from(e).toString("base64"):btoa(encodeURIComponent(e).replace(/%([0-9A-F]{2})/g,(r,t)=>String.fromCharCode(parseInt(t,16))))}function z(e,r,t){if(t.type==="bearer"&&(e.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=ie(`${t.username}:${t.password}`);e.Authorization=`Basic ${n}`}t.type==="apikey"&&(t.in==="query"?r[t.key]=t.value:e[t.key]=t.value)}function X(e){return e==null?null:typeof e=="number"?{count:e,delay:300,backoff:"exponential"}:e}function Y(e,r,t){return t>=r.count||e.type==="ABORT_ERROR"?!1:r.on&&e.status?r.on.includes(e.status):!!(e.type==="NETWORK_ERROR"||e.type==="TIMEOUT_ERROR"||e.status&&e.status>=500)}async function J(e,r){let t=e.delay??300,n=e.backoff==="exponential"?t*Math.pow(2,r):t*(r+1);await new Promise(u=>setTimeout(u,n))}var m=new Map,ae=1e3;function k(e,r){return r?.key??e}function Z(e){let r=m.get(e);if(!r)return null;if(Date.now()>r.expiresAt)return m.delete(e),null;let t=r.response.data;return t instanceof ArrayBuffer&&(t=t.slice(0)),{...r.response,data:t,fromCache:!0}}function Q(e,r,t){if(m.size>=ae&&!m.has(e)){let n=m.keys().next().value;n!==void 0&&m.delete(n)}m.set(e,{response:r,expiresAt:Date.now()+t.ttl})}function le(){m.clear()}var A=new Map;function V(e){return A.get(e)??null}function ee(e,r){A.set(e,r),r.finally(()=>A.delete(e))}function re(e,r){console.group(`[hurl] \u2192 ${r.method??"GET"} ${e}`),r.headers&&Object.keys(r.headers).length>0&&console.log("headers:",r.headers),r.query&&console.log("query:",r.query),r.body&&console.log("body:",r.body),r.timeout&&console.log("timeout:",r.timeout),r.retry&&console.log("retry:",r.retry),console.groupEnd()}function U(e){let r=e.status>=400?"\u{1F534}":e.status>=300?"\u{1F7E1}":"\u{1F7E2}";console.group(`[hurl] ${r} ${e.status} ${e.statusText} (${e.timing.duration}ms)`),console.log("requestId:",e.requestId),e.fromCache&&console.log("served from cache"),console.log("headers:",e.headers),console.log("data:",e.data),console.groupEnd()}function te(e){console.group("[hurl] \u{1F534} Error"),console.error(e),console.groupEnd()}function ce(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(36).slice(2,10)}function pe(e,r,t){let n;if(r.startsWith("http://")||r.startsWith("https://")){if(e){let s=new URL(e).origin,a=new URL(r).origin;if(s!==a)throw new Error(`Absolute URL "${r}" does not match baseUrl origin "${s}". Pass the full URL without baseUrl, or use a path-relative URL.`)}n=r}else{if(r.startsWith("//"))throw new Error("Protocol-relative URLs are not supported. Use an explicit https:// or http:// scheme.");n=e?`${e.replace(/\/$/,"")}/${r.replace(/^\//,"")}`:r}if(!t||Object.keys(t).length===0)return n;let u=new URLSearchParams;for(let[s,a]of Object.entries(t))u.set(s,String(a));return`${n}?${u.toString()}`}function fe(e){return e instanceof ReadableStream||e!==null&&typeof e=="object"&&typeof e.pipe=="function"}function de(e,r){let t={...r.headers,...e.headers},n=e.body;return n!=null&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&!(n instanceof ArrayBuffer)&&!fe(n)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function ge(e){if(e!=null)return e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||typeof e=="string"||e instanceof ReadableStream||typeof e.pipe=="function"?e:JSON.stringify(e)}async function ne(e,r,t){let n=r.requestId??ce(),u=r.method??"GET",s=Date.now(),a=X(r.retry??t.retry),o=r.debug??t.debug??!1,i=r.throwOnError??t.throwOnError??!0,c={...t.query,...r.query},E=de(r,t),T=r.timeout??t.timeout,w=r.auth??t.auth;w&&z(E,c,w);let R=pe(t.baseUrl??"",e,Object.keys(c).length>0?c:void 0);(r.proxy??t.proxy)&&o&&console.warn("[hurl] proxy option is not supported with native fetch. Use HTTP_PROXY/HTTPS_PROXY env vars in Node.js.");let l=r.cache??t.cache,H=!!l&&!l.bypass&&u==="GET";if(H){let f=k(R,l),h=Z(f);if(h)return o&&U(h),h}let B=r.deduplicate??t.deduplicate??!1;if(B&&u==="GET"){let f=V(R);if(f)return f}o&&re(R,{...r,method:u});let v=async f=>{let h=null,$=!1,x=new AbortController,y=r.signal,I=null;y&&(y.aborted?x.abort(y.reason):(I=()=>x.abort(y.reason),y.addEventListener("abort",I,{once:!0}))),T&&(h=setTimeout(()=>{$=!0,x.abort()},T));try{let d=ge(r.body),g=r.onUploadProgress??t.onUploadProgress;d!==void 0&&g&&(r.body instanceof FormData?o&&console.warn("[hurl] onUploadProgress is not supported for FormData bodies. Use XMLHttpRequest for FormData upload progress."):d=N(d,g));let b=await fetch(R,{method:u,headers:E,body:d,signal:x.signal,redirect:r.followRedirects??!0?"follow":"manual"}),L=await W(b,n,u,r.stream??!1,r.onDownloadProgress??t.onDownloadProgress);if(!b.ok&&i)throw _({status:b.status,statusText:b.statusText,data:L,headers:C(b.headers),requestId:n,retries:f});let q=G(L,b,n,s);return H&&l&&Q(k(R,l),q,l),o&&U(q),q}catch(d){let g;if(d instanceof p?g=d:d.name==="AbortError"||d.code==="ABORT_ERR"?g=$?M(T,n):K(n):g=F(d.message,n),g.retries=f,a&&Y(g,a,f))return o&&console.log(`[hurl] retrying (${f+1}/${a.count})...`),await J(a,f),v(f+1);throw o&&te(g),g}finally{h&&clearTimeout(h),I&&y&&y.removeEventListener("abort",I)}},S=v(0);return B&&u==="GET"&&ee(R,S),S}function O(){let e=[];return{use(r){return e.push(r),()=>{let t=e.indexOf(r);t!==-1&&e.splice(t,1)}},clear(){e.length=0},getAll(){return[...e]}}}async function oe(e,r,t){let n={url:r,options:t};for(let u of e)n=await u(n.url,n.options);return n}async function se(e,r){let t=r;for(let n of e)t=await n(t);return t}async function ue(e,r){let t=r;for(let n of e)t instanceof p&&(t=await n(t));return t}function D(e={}){let r={...e},t=O(),n=O(),u=O();async function s(o,i={}){let c=o,E=i,T=t.getAll(),w=n.getAll(),R=u.getAll();if(T.length>0){let l=await oe(T,o,i);c=l.url,E=l.options}try{let l=await ne(c,E,r);return w.length>0?await se(w,l):l}catch(l){if(l instanceof p&&R.length>0){let H=await ue(R,l);if(!(H instanceof p))return H;throw H}throw l}}return{request:s,get(o,i){return s(o,{...i,method:"GET"})},post(o,i,c){return s(o,{...c,method:"POST",body:i})},put(o,i,c){return s(o,{...c,method:"PUT",body:i})},patch(o,i,c){return s(o,{...c,method:"PATCH",body:i})},delete(o,i){return s(o,{...i,method:"DELETE"})},head(o,i){return s(o,{...i,method:"HEAD"})},options(o,i){return s(o,{...i,method:"OPTIONS"})},all(o){return Promise.all(o)},defaults:{set(o){r={...r,...o}},get(){return{...r}},reset(){r={...e}}},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:u.use.bind(u),clear:u.clear.bind(u)}},create(o){return D({...r,...o})},extend(o){return D({...r,...o})}}}var Re=D(),sr=Re;export{p as HurlError,le as clearCache,D as createInstance,sr as default};
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.6",
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
+ }