@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 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 10KB 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,61 +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
- 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
- hurl.defaults.set({
107
- auth: { type: 'bearer', token: 'my-token' }
108
- })
133
+ // Bearer token
134
+ hurl.defaults.set({ auth: { type: 'bearer', token: 'my-token' } })
109
135
 
110
- hurl.defaults.set({
111
- auth: { type: 'basic', username: 'admin', password: 'secret' }
112
- })
136
+ // Basic auth
137
+ hurl.defaults.set({ auth: { type: 'basic', username: 'admin', password: 'secret' } })
113
138
 
114
- hurl.defaults.set({
115
- auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' }
116
- })
139
+ // API key (header)
140
+ hurl.defaults.set({ auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' } })
117
141
 
118
- hurl.defaults.set({
119
- auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' }
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
- ## Retry
146
+ ---
147
+
148
+ ## Retry & Backoff
124
149
 
125
150
  ```ts
126
- await hurl.get('/users', {
127
- retry: 3
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
- `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
+ ---
141
168
 
142
- ## Timeout and Abort
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
- remove()
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
- ## File Upload with Progress
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 only applies to GET requests. Responses are stored in memory with a TTL in milliseconds.
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
- cache: { ttl: 60000 }
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
- `create()` produces a fully isolated instance — no shared defaults, interceptors, or state with the parent. `extend()` merges the provided defaults on top of the parent's and inherits all of the parent's interceptors.
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, 5xx), network failures, timeouts, aborts, and parse failures. It never resolves silently on bad status codes.
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 resolves normally and you can check `res.status` yourself.
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
- ## Environment Support
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
- ## API Reference
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
- ### hurl.options(url, options?)
406
- Sends an OPTIONS 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
+ ```
407
442
 
408
- ### hurl.request(url, options?)
409
- Sends a request with the method specified in options. Defaults to GET. Returns `Promise<HurlResponse<T>>`.
443
+ ---
410
444
 
411
- ### hurl.all(requests)
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
- ### hurl.create(defaults?)
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
- ### hurl.extend(defaults?)
418
- Creates a new instance that inherits the current defaults, merges in the provided ones, and copies all parent interceptors (request, response, and error).
449
+ | Runtime | Support |
450
+ |---|:---:|
451
+ | Node.js 18+ | ✅ |
452
+ | Cloudflare Workers | ✅ |
453
+ | Vercel Edge Functions | ✅ |
454
+ | Deno | ✅ |
455
+ | Bun | ✅ |
419
456
 
420
- ### hurl.defaults.set(defaults)
421
- Sets global defaults for the current instance. Merged into every request.
457
+ Exports both ESM (`import`) and CommonJS (`require`).
422
458
 
423
- ### hurl.defaults.get()
424
- Returns the current defaults object.
459
+ ---
425
460
 
426
- ### hurl.defaults.reset()
427
- Resets defaults to the values provided when the instance was created.
461
+ ## Why Not Axios?
428
462
 
429
- ### hurl.interceptors.request.use(fn)
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
- ### hurl.interceptors.response.use(fn)
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
- ### hurl.interceptors.error.use(fn)
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
- ### clearCache()
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
- ```ts
442
- import { clearCache } from '@firekid/hurl'
443
- clearCache()
444
- ```
471
+ **request** has been deprecated since 2020.
445
472
 
446
- ### invalidateCache(key)
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
- ```ts
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
- ## License
477
+ ## API Reference
456
478
 
457
- 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.7",
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
+ }