@firekid/hurl 1.0.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 +21 -0
- package/README.md +405 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 hurl contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# hurl
|
|
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.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @firekid/hurl
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
`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.
|
|
12
|
+
|
|
13
|
+
## Core Concepts
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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()`.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @firekid/hurl
|
|
25
|
+
yarn add @firekid/hurl
|
|
26
|
+
pnpm add @firekid/hurl
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import hurl from '@firekid/hurl'
|
|
33
|
+
|
|
34
|
+
const res = await hurl.get('https://api.example.com/users')
|
|
35
|
+
|
|
36
|
+
res.data // parsed response body
|
|
37
|
+
res.status // 200
|
|
38
|
+
res.headers // Record<string, string>
|
|
39
|
+
res.requestId // unique ID for this request
|
|
40
|
+
res.timing // { start, end, duration }
|
|
41
|
+
res.fromCache // boolean
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## HTTP Methods
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
hurl.get<T>(url, options?)
|
|
48
|
+
hurl.post<T>(url, body?, options?)
|
|
49
|
+
hurl.put<T>(url, body?, options?)
|
|
50
|
+
hurl.patch<T>(url, body?, options?)
|
|
51
|
+
hurl.delete<T>(url, options?)
|
|
52
|
+
hurl.head(url, options?)
|
|
53
|
+
hurl.options<T>(url, options?)
|
|
54
|
+
hurl.request<T>(url, options?)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Global Defaults
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
hurl.defaults.set({
|
|
61
|
+
baseUrl: 'https://api.example.com',
|
|
62
|
+
headers: { 'x-api-version': '2' },
|
|
63
|
+
timeout: 10000,
|
|
64
|
+
retry: 3,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
hurl.defaults.get()
|
|
68
|
+
hurl.defaults.reset()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Request Options
|
|
72
|
+
|
|
73
|
+
All methods accept a `HurlRequestOptions` object.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
type HurlRequestOptions = {
|
|
77
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
78
|
+
headers?: Record<string, string>
|
|
79
|
+
body?: unknown
|
|
80
|
+
query?: Record<string, string | number | boolean>
|
|
81
|
+
timeout?: number
|
|
82
|
+
retry?: RetryConfig | number
|
|
83
|
+
auth?: AuthConfig
|
|
84
|
+
proxy?: ProxyConfig
|
|
85
|
+
cache?: CacheConfig
|
|
86
|
+
signal?: AbortSignal
|
|
87
|
+
followRedirects?: boolean
|
|
88
|
+
maxRedirects?: number
|
|
89
|
+
onUploadProgress?: ProgressCallback
|
|
90
|
+
onDownloadProgress?: ProgressCallback
|
|
91
|
+
stream?: boolean
|
|
92
|
+
debug?: boolean
|
|
93
|
+
requestId?: string
|
|
94
|
+
deduplicate?: boolean
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Authentication
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
hurl.defaults.set({
|
|
102
|
+
auth: { type: 'bearer', token: 'my-token' }
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
hurl.defaults.set({
|
|
106
|
+
auth: { type: 'basic', username: 'admin', password: 'secret' }
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
hurl.defaults.set({
|
|
110
|
+
auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
hurl.defaults.set({
|
|
114
|
+
auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' }
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Retry
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await hurl.get('/users', {
|
|
122
|
+
retry: 3
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await hurl.get('/users', {
|
|
126
|
+
retry: {
|
|
127
|
+
count: 3,
|
|
128
|
+
delay: 300,
|
|
129
|
+
backoff: 'exponential',
|
|
130
|
+
on: [500, 502, 503],
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`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.
|
|
136
|
+
|
|
137
|
+
## Timeout and Abort
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
await hurl.get('/users', { timeout: 5000 })
|
|
141
|
+
|
|
142
|
+
const controller = new AbortController()
|
|
143
|
+
setTimeout(() => controller.abort(), 3000)
|
|
144
|
+
await hurl.get('/users', { signal: controller.signal })
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Interceptors
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const remove = hurl.interceptors.request.use((url, options) => {
|
|
151
|
+
return {
|
|
152
|
+
url,
|
|
153
|
+
options: {
|
|
154
|
+
...options,
|
|
155
|
+
headers: { ...options.headers, 'x-trace-id': crypto.randomUUID() },
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
remove()
|
|
161
|
+
|
|
162
|
+
hurl.interceptors.response.use((response) => {
|
|
163
|
+
console.log(response.status, response.timing.duration)
|
|
164
|
+
return response
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
hurl.interceptors.error.use((error) => {
|
|
168
|
+
if (error.status === 401) redirectToLogin()
|
|
169
|
+
return error
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
hurl.interceptors.request.clear()
|
|
173
|
+
hurl.interceptors.response.clear()
|
|
174
|
+
hurl.interceptors.error.clear()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## File Upload with Progress
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const form = new FormData()
|
|
181
|
+
form.append('file', file)
|
|
182
|
+
|
|
183
|
+
await hurl.post('/upload', form, {
|
|
184
|
+
onUploadProgress: ({ loaded, total, percent }) => {
|
|
185
|
+
console.log(`${percent}%`)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Download Progress
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
await hurl.get('/large-file', {
|
|
194
|
+
onDownloadProgress: ({ loaded, total, percent }) => {
|
|
195
|
+
console.log(`${percent}%`)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Caching
|
|
201
|
+
|
|
202
|
+
Caching only applies to GET requests. Responses are stored in memory with a TTL in milliseconds.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
await hurl.get('/users', {
|
|
206
|
+
cache: { ttl: 60000 }
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await hurl.get('/users', {
|
|
210
|
+
cache: { ttl: 60000, key: 'all-users' }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await hurl.get('/users', {
|
|
214
|
+
cache: { ttl: 60000, bypass: true }
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Request Deduplication
|
|
219
|
+
|
|
220
|
+
When `deduplicate` is true and the same GET URL is called multiple times simultaneously, only one network request is made.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const [a, b] = await Promise.all([
|
|
224
|
+
hurl.get('/users', { deduplicate: true }),
|
|
225
|
+
hurl.get('/users', { deduplicate: true }),
|
|
226
|
+
])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Proxy
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
await hurl.get('/users', {
|
|
233
|
+
proxy: { url: 'http://proxy.example.com:8080' }
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
await hurl.get('/users', {
|
|
237
|
+
proxy: {
|
|
238
|
+
url: 'socks5://proxy.example.com:1080',
|
|
239
|
+
auth: { username: 'user', password: 'pass' }
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Parallel Requests
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const [users, posts] = await hurl.all([
|
|
248
|
+
hurl.get('/users'),
|
|
249
|
+
hurl.get('/posts'),
|
|
250
|
+
])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Isolated Instances
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const api = hurl.create({
|
|
257
|
+
baseUrl: 'https://api.example.com',
|
|
258
|
+
auth: { type: 'bearer', token: 'my-token' },
|
|
259
|
+
timeout: 5000,
|
|
260
|
+
retry: 3,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await api.get('/users')
|
|
264
|
+
|
|
265
|
+
const adminApi = api.extend({
|
|
266
|
+
headers: { 'x-role': 'admin' }
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Debug Mode
|
|
271
|
+
|
|
272
|
+
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.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
await hurl.get('/users', { debug: true })
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Error Handling
|
|
279
|
+
|
|
280
|
+
`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
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import hurl, { HurlError } from '@firekid/hurl'
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await hurl.get('/users')
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err instanceof HurlError) {
|
|
289
|
+
err.type // 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR'
|
|
290
|
+
err.status // 404
|
|
291
|
+
err.statusText // 'Not Found'
|
|
292
|
+
err.data // parsed error response body
|
|
293
|
+
err.headers // response headers
|
|
294
|
+
err.requestId // same ID as the request
|
|
295
|
+
err.retries // number of retries attempted
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## TypeScript
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
type User = { id: number; name: string }
|
|
304
|
+
|
|
305
|
+
const res = await hurl.get<User[]>('/users')
|
|
306
|
+
res.data
|
|
307
|
+
|
|
308
|
+
const created = await hurl.post<User>('/users', { name: 'John' })
|
|
309
|
+
created.data.id
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Response Shape
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
type HurlResponse<T> = {
|
|
316
|
+
data: T
|
|
317
|
+
status: number
|
|
318
|
+
statusText: string
|
|
319
|
+
headers: Record<string, string>
|
|
320
|
+
requestId: string
|
|
321
|
+
timing: {
|
|
322
|
+
start: number
|
|
323
|
+
end: number
|
|
324
|
+
duration: number
|
|
325
|
+
}
|
|
326
|
+
fromCache: boolean
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Environment Support
|
|
331
|
+
|
|
332
|
+
`hurl` runs anywhere the Fetch API is available.
|
|
333
|
+
|
|
334
|
+
- Node.js 18 and above
|
|
335
|
+
- Cloudflare Workers
|
|
336
|
+
- Vercel Edge Functions
|
|
337
|
+
- Deno
|
|
338
|
+
- Bun
|
|
339
|
+
|
|
340
|
+
Exports both ESM (`import`) and CommonJS (`require`).
|
|
341
|
+
|
|
342
|
+
## API Reference
|
|
343
|
+
|
|
344
|
+
### hurl.get(url, options?)
|
|
345
|
+
Sends a GET request. Returns `Promise<HurlResponse<T>>`.
|
|
346
|
+
|
|
347
|
+
### hurl.post(url, body?, options?)
|
|
348
|
+
Sends a POST request. Body is auto-serialized to JSON if it is a plain object. Returns `Promise<HurlResponse<T>>`.
|
|
349
|
+
|
|
350
|
+
### hurl.put(url, body?, options?)
|
|
351
|
+
Sends a PUT request. Returns `Promise<HurlResponse<T>>`.
|
|
352
|
+
|
|
353
|
+
### hurl.patch(url, body?, options?)
|
|
354
|
+
Sends a PATCH request. Returns `Promise<HurlResponse<T>>`.
|
|
355
|
+
|
|
356
|
+
### hurl.delete(url, options?)
|
|
357
|
+
Sends a DELETE request. Returns `Promise<HurlResponse<T>>`.
|
|
358
|
+
|
|
359
|
+
### hurl.head(url, options?)
|
|
360
|
+
Sends a HEAD request. Returns `Promise<HurlResponse<void>>`.
|
|
361
|
+
|
|
362
|
+
### hurl.options(url, options?)
|
|
363
|
+
Sends an OPTIONS request. Returns `Promise<HurlResponse<T>>`.
|
|
364
|
+
|
|
365
|
+
### hurl.request(url, options?)
|
|
366
|
+
Sends a request with the method specified in options. Defaults to GET. Returns `Promise<HurlResponse<T>>`.
|
|
367
|
+
|
|
368
|
+
### hurl.all(requests)
|
|
369
|
+
Runs an array of requests in parallel. Returns a promise that resolves when all requests complete. Equivalent to `Promise.all`.
|
|
370
|
+
|
|
371
|
+
### hurl.create(defaults?)
|
|
372
|
+
Creates a new isolated instance with its own defaults, interceptors, and state. Does not share anything with the parent instance.
|
|
373
|
+
|
|
374
|
+
### hurl.extend(defaults?)
|
|
375
|
+
Creates a new instance that inherits the current defaults and merges in the provided ones.
|
|
376
|
+
|
|
377
|
+
### hurl.defaults.set(defaults)
|
|
378
|
+
Sets global defaults for the current instance. Merged into every request.
|
|
379
|
+
|
|
380
|
+
### hurl.defaults.get()
|
|
381
|
+
Returns the current defaults object.
|
|
382
|
+
|
|
383
|
+
### hurl.defaults.reset()
|
|
384
|
+
Resets defaults to the values provided when the instance was created.
|
|
385
|
+
|
|
386
|
+
### hurl.interceptors.request.use(fn)
|
|
387
|
+
Registers a request interceptor. Returns a function that removes the interceptor when called.
|
|
388
|
+
|
|
389
|
+
### hurl.interceptors.response.use(fn)
|
|
390
|
+
Registers a response interceptor. Returns a function that removes the interceptor when called.
|
|
391
|
+
|
|
392
|
+
### hurl.interceptors.error.use(fn)
|
|
393
|
+
Registers an error interceptor. Returns a function that removes the interceptor when called.
|
|
394
|
+
|
|
395
|
+
### clearCache()
|
|
396
|
+
Clears the entire in-memory response cache.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { clearCache } from '@firekid/hurl'
|
|
400
|
+
clearCache()
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## License
|
|
404
|
+
|
|
405
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
2
|
+
type AuthConfig = {
|
|
3
|
+
type: 'bearer';
|
|
4
|
+
token: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'basic';
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'apikey';
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
in?: 'header' | 'query';
|
|
14
|
+
};
|
|
15
|
+
type ProxyConfig = {
|
|
16
|
+
url: string;
|
|
17
|
+
auth?: {
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type RetryConfig = {
|
|
23
|
+
count: number;
|
|
24
|
+
delay?: number;
|
|
25
|
+
backoff?: 'linear' | 'exponential';
|
|
26
|
+
on?: number[];
|
|
27
|
+
};
|
|
28
|
+
type CacheConfig = {
|
|
29
|
+
ttl: number;
|
|
30
|
+
key?: string;
|
|
31
|
+
bypass?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type ProgressCallback = (e: {
|
|
34
|
+
loaded: number;
|
|
35
|
+
total: number;
|
|
36
|
+
percent: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
type HurlRequestOptions = {
|
|
39
|
+
method?: Method;
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
body?: unknown;
|
|
42
|
+
query?: Record<string, string | number | boolean>;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
retry?: RetryConfig | number;
|
|
45
|
+
auth?: AuthConfig;
|
|
46
|
+
proxy?: ProxyConfig;
|
|
47
|
+
cache?: CacheConfig;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
followRedirects?: boolean;
|
|
50
|
+
maxRedirects?: number;
|
|
51
|
+
onUploadProgress?: ProgressCallback;
|
|
52
|
+
onDownloadProgress?: ProgressCallback;
|
|
53
|
+
stream?: boolean;
|
|
54
|
+
debug?: boolean;
|
|
55
|
+
requestId?: string;
|
|
56
|
+
deduplicate?: boolean;
|
|
57
|
+
};
|
|
58
|
+
type HurlDefaults = Omit<HurlRequestOptions, 'body' | 'method'> & {
|
|
59
|
+
baseUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
type HurlResponse<T = unknown> = {
|
|
62
|
+
data: T;
|
|
63
|
+
status: number;
|
|
64
|
+
statusText: string;
|
|
65
|
+
headers: Record<string, string>;
|
|
66
|
+
requestId: string;
|
|
67
|
+
timing: {
|
|
68
|
+
start: number;
|
|
69
|
+
end: number;
|
|
70
|
+
duration: number;
|
|
71
|
+
};
|
|
72
|
+
fromCache: boolean;
|
|
73
|
+
};
|
|
74
|
+
type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<{
|
|
75
|
+
url: string;
|
|
76
|
+
options: HurlRequestOptions;
|
|
77
|
+
}> | {
|
|
78
|
+
url: string;
|
|
79
|
+
options: HurlRequestOptions;
|
|
80
|
+
};
|
|
81
|
+
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
|
+
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
84
|
+
declare class HurlError extends Error {
|
|
85
|
+
type: HurlErrorType;
|
|
86
|
+
status?: number;
|
|
87
|
+
statusText?: string;
|
|
88
|
+
data?: unknown;
|
|
89
|
+
headers?: Record<string, string>;
|
|
90
|
+
requestId: string;
|
|
91
|
+
retries: number;
|
|
92
|
+
constructor(params: {
|
|
93
|
+
message: string;
|
|
94
|
+
type: HurlErrorType;
|
|
95
|
+
status?: number;
|
|
96
|
+
statusText?: string;
|
|
97
|
+
data?: unknown;
|
|
98
|
+
headers?: Record<string, string>;
|
|
99
|
+
requestId: string;
|
|
100
|
+
retries?: number;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
type HurlInstance = {
|
|
104
|
+
get<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
105
|
+
post<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
106
|
+
put<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
107
|
+
patch<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
108
|
+
delete<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
109
|
+
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
|
+
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
|
+
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
112
|
+
all<T extends unknown[]>(requests: {
|
|
113
|
+
[K in keyof T]: Promise<T[K]>;
|
|
114
|
+
}): Promise<T>;
|
|
115
|
+
defaults: {
|
|
116
|
+
set(d: HurlDefaults): void;
|
|
117
|
+
get(): HurlDefaults;
|
|
118
|
+
reset(): void;
|
|
119
|
+
};
|
|
120
|
+
interceptors: {
|
|
121
|
+
request: {
|
|
122
|
+
use(fn: RequestInterceptor): () => void;
|
|
123
|
+
clear(): void;
|
|
124
|
+
};
|
|
125
|
+
response: {
|
|
126
|
+
use(fn: ResponseInterceptor): () => void;
|
|
127
|
+
clear(): void;
|
|
128
|
+
};
|
|
129
|
+
error: {
|
|
130
|
+
use(fn: ErrorInterceptor): () => void;
|
|
131
|
+
clear(): void;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
create(defaults?: HurlDefaults): HurlInstance;
|
|
135
|
+
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
declare function clearCache(): void;
|
|
139
|
+
|
|
140
|
+
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
|
+
declare const hurl: HurlInstance;
|
|
142
|
+
|
|
143
|
+
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
2
|
+
type AuthConfig = {
|
|
3
|
+
type: 'bearer';
|
|
4
|
+
token: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'basic';
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'apikey';
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
in?: 'header' | 'query';
|
|
14
|
+
};
|
|
15
|
+
type ProxyConfig = {
|
|
16
|
+
url: string;
|
|
17
|
+
auth?: {
|
|
18
|
+
username: string;
|
|
19
|
+
password: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type RetryConfig = {
|
|
23
|
+
count: number;
|
|
24
|
+
delay?: number;
|
|
25
|
+
backoff?: 'linear' | 'exponential';
|
|
26
|
+
on?: number[];
|
|
27
|
+
};
|
|
28
|
+
type CacheConfig = {
|
|
29
|
+
ttl: number;
|
|
30
|
+
key?: string;
|
|
31
|
+
bypass?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type ProgressCallback = (e: {
|
|
34
|
+
loaded: number;
|
|
35
|
+
total: number;
|
|
36
|
+
percent: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
type HurlRequestOptions = {
|
|
39
|
+
method?: Method;
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
body?: unknown;
|
|
42
|
+
query?: Record<string, string | number | boolean>;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
retry?: RetryConfig | number;
|
|
45
|
+
auth?: AuthConfig;
|
|
46
|
+
proxy?: ProxyConfig;
|
|
47
|
+
cache?: CacheConfig;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
followRedirects?: boolean;
|
|
50
|
+
maxRedirects?: number;
|
|
51
|
+
onUploadProgress?: ProgressCallback;
|
|
52
|
+
onDownloadProgress?: ProgressCallback;
|
|
53
|
+
stream?: boolean;
|
|
54
|
+
debug?: boolean;
|
|
55
|
+
requestId?: string;
|
|
56
|
+
deduplicate?: boolean;
|
|
57
|
+
};
|
|
58
|
+
type HurlDefaults = Omit<HurlRequestOptions, 'body' | 'method'> & {
|
|
59
|
+
baseUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
type HurlResponse<T = unknown> = {
|
|
62
|
+
data: T;
|
|
63
|
+
status: number;
|
|
64
|
+
statusText: string;
|
|
65
|
+
headers: Record<string, string>;
|
|
66
|
+
requestId: string;
|
|
67
|
+
timing: {
|
|
68
|
+
start: number;
|
|
69
|
+
end: number;
|
|
70
|
+
duration: number;
|
|
71
|
+
};
|
|
72
|
+
fromCache: boolean;
|
|
73
|
+
};
|
|
74
|
+
type RequestInterceptor = (url: string, options: HurlRequestOptions) => Promise<{
|
|
75
|
+
url: string;
|
|
76
|
+
options: HurlRequestOptions;
|
|
77
|
+
}> | {
|
|
78
|
+
url: string;
|
|
79
|
+
options: HurlRequestOptions;
|
|
80
|
+
};
|
|
81
|
+
type ResponseInterceptor<T = unknown> = (response: HurlResponse<T>) => Promise<HurlResponse<T>> | HurlResponse<T>;
|
|
82
|
+
type ErrorInterceptor = (error: HurlError) => Promise<HurlError | HurlResponse> | HurlError | HurlResponse;
|
|
83
|
+
type HurlErrorType = 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR';
|
|
84
|
+
declare class HurlError extends Error {
|
|
85
|
+
type: HurlErrorType;
|
|
86
|
+
status?: number;
|
|
87
|
+
statusText?: string;
|
|
88
|
+
data?: unknown;
|
|
89
|
+
headers?: Record<string, string>;
|
|
90
|
+
requestId: string;
|
|
91
|
+
retries: number;
|
|
92
|
+
constructor(params: {
|
|
93
|
+
message: string;
|
|
94
|
+
type: HurlErrorType;
|
|
95
|
+
status?: number;
|
|
96
|
+
statusText?: string;
|
|
97
|
+
data?: unknown;
|
|
98
|
+
headers?: Record<string, string>;
|
|
99
|
+
requestId: string;
|
|
100
|
+
retries?: number;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
type HurlInstance = {
|
|
104
|
+
get<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
105
|
+
post<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
106
|
+
put<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
107
|
+
patch<T = unknown>(url: string, body?: unknown, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
108
|
+
delete<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
109
|
+
head(url: string, options?: HurlRequestOptions): Promise<HurlResponse<void>>;
|
|
110
|
+
options<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
111
|
+
request<T = unknown>(url: string, options?: HurlRequestOptions): Promise<HurlResponse<T>>;
|
|
112
|
+
all<T extends unknown[]>(requests: {
|
|
113
|
+
[K in keyof T]: Promise<T[K]>;
|
|
114
|
+
}): Promise<T>;
|
|
115
|
+
defaults: {
|
|
116
|
+
set(d: HurlDefaults): void;
|
|
117
|
+
get(): HurlDefaults;
|
|
118
|
+
reset(): void;
|
|
119
|
+
};
|
|
120
|
+
interceptors: {
|
|
121
|
+
request: {
|
|
122
|
+
use(fn: RequestInterceptor): () => void;
|
|
123
|
+
clear(): void;
|
|
124
|
+
};
|
|
125
|
+
response: {
|
|
126
|
+
use(fn: ResponseInterceptor): () => void;
|
|
127
|
+
clear(): void;
|
|
128
|
+
};
|
|
129
|
+
error: {
|
|
130
|
+
use(fn: ErrorInterceptor): () => void;
|
|
131
|
+
clear(): void;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
create(defaults?: HurlDefaults): HurlInstance;
|
|
135
|
+
extend(defaults?: HurlDefaults): HurlInstance;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
declare function clearCache(): void;
|
|
139
|
+
|
|
140
|
+
declare function createInstance(initialDefaults?: HurlDefaults): HurlInstance;
|
|
141
|
+
declare const hurl: HurlInstance;
|
|
142
|
+
|
|
143
|
+
export { type ErrorInterceptor, type HurlDefaults, HurlError, type HurlInstance, type HurlRequestOptions, type HurlResponse, type RequestInterceptor, type ResponseInterceptor, clearCache, createInstance, hurl as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var I=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var oe=Object.prototype.hasOwnProperty;var ue=(e,r)=>{for(var t in r)I(e,t,{get:r[t],enumerable:!0})},ie=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of se(r))!oe.call(e,s)&&s!==t&&I(e,s,{get:()=>r[s],enumerable:!(n=ne(r,s))||n.enumerable});return e};var le=e=>ie(I({},"__esModule",{value:!0}),e);var Re={};ue(Re,{HurlError:()=>l,clearCache:()=>J,createInstance:()=>O,default:()=>ge});module.exports=le(Re);var l=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 l({message:`HTTP ${e.status}: ${e.statusText}`,type:"HTTP_ERROR",...e})}function v(e,r){return new l({message:e,type:"NETWORK_ERROR",requestId:r})}function F(e,r){return new l({message:`Request timed out after ${e}ms`,type:"TIMEOUT_ERROR",requestId:r})}function S(e){return new l({message:"Request was aborted",type:"ABORT_ERROR",requestId:e})}function B(e,r){return new l({message:`Failed to parse response: ${e}`,type:"PARSE_ERROR",requestId:r})}async function _(e,r){let t=e.body?.getReader(),n=parseInt(e.headers.get("content-length")??"0",10);if(!t)return e.text();let s=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;s.push(a),i+=a.length,r({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let c=new Uint8Array(i),o=0;for(let u of s)c.set(u,o),o+=u.length;return new TextDecoder().decode(c)}function P(e){let r={};return e.forEach((t,n)=>{r[n]=t}),r}async function K(e,r,t){let n=e.headers.get("content-type")??"";try{if(t){let s=await _(e,t);return n.includes("application/json")?JSON.parse(s):s}return n.includes("application/json")?await e.json():n.includes("text/")?await e.text():n.includes("application/octet-stream")||n.includes("image/")?await e.arrayBuffer():await e.text()}catch(s){throw B(s.message,r)}}function L(e,r,t,n){let s=Date.now();return{data:e,status:r.status,statusText:r.statusText,headers:P(r.headers),requestId:t,timing:{start:n,end:s,duration:s-n},fromCache:!1}}function M(e,r,t){if(t.type==="bearer"&&(e.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=btoa(`${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 j(e){return e==null?null:typeof e=="number"?{count:e,delay:300,backoff:"exponential"}:e}function N(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 G(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 x=new Map;function k(e,r){return r?.key??e}function W(e){let r=x.get(e);return r?Date.now()>r.expiresAt?(x.delete(e),null):{...r.response,fromCache:!0}:null}function z(e,r,t){x.set(e,{response:r,expiresAt:Date.now()+t.ttl})}function J(){x.clear()}var C=new Map;function Q(e){return C.get(e)??null}function V(e,r){C.set(e,r),r.finally(()=>C.delete(e))}function X(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 A(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 Y(e){console.group("[hurl] \u{1F534} Error"),console.error(e),console.groupEnd()}function ae(){return Math.random().toString(36).slice(2,10)}function ce(e,r,t){let n;if(r.startsWith("http://")||r.startsWith("https://")){if(e){let i=new URL(e).origin,c=new URL(r).origin;if(i!==c)throw new Error(`Absolute URL "${r}" does not match baseUrl origin "${i}". 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[i,c]of Object.entries(t))s.set(i,String(c));return`${n}?${s.toString()}`}function pe(e,r){let t={...r.headers,...e.headers},n=e.body;return n&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function fe(e){if(e!=null)return e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||typeof e=="string"?e:JSON.stringify(e)}async function Z(e,r,t){let n=r.requestId??ae(),s=r.method??"GET",i=Date.now(),c=j(r.retry??t.retry),o=r.debug??t.debug??!1,u={...t.query,...r.query},a=pe(r,t),h=r.timeout??t.timeout,H=r.auth??t.auth;H&&M(a,u,H);let R=ce(t.baseUrl??"",e,Object.keys(u).length>0?u:void 0),m=r.cache??t.cache,p=!!m&&!m.bypass&&s==="GET";if(p){let d=k(R,m),T=W(d);if(T)return o&&A(T),T}let y=r.deduplicate??t.deduplicate??!1;if(y&&s==="GET"){let d=Q(R);if(d)return d}o&&X(R,{...r,method:s});let D=async d=>{let T=[],b=null,E=new AbortController;T.push(E),r.signal&&r.signal.addEventListener("abort",()=>E.abort()),h&&(b=setTimeout(()=>E.abort("timeout"),h));try{let f=await fetch(R,{method:s,headers:a,body:fe(r.body),signal:E.signal,redirect:r.followRedirects??!0?"follow":"manual"});b&&clearTimeout(b);let g=await K(f,n,r.onDownloadProgress);if(!f.ok)throw $({status:f.status,statusText:f.statusText,data:g,headers:P(f.headers),requestId:n,retries:d});let w=L(g,f,n,i);return p&&m&&z(k(R,m),w,m),o&&A(w),w}catch(f){b&&clearTimeout(b);let g;if(f instanceof l?g=f:f.name==="AbortError"?g=h&&f.message==="timeout"?F(h,n):S(n):g=v(f.message,n),g.retries=d,c&&N(g,c,d))return o&&console.log(`[hurl] retrying (${d+1}/${c.count})...`),await G(c,d),D(d+1);throw o&&Y(g),g}},U=D(0);return y&&s==="GET"&&V(R,U),U}function q(){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 ee(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 re(e,r){let t=r;for(let n of e)t=await n(t);return t}async function te(e,r){let t=r;for(let n of e)t instanceof l&&(t=await n(t));return t}function O(e={}){let r={...e},t=q(),n=q(),s=q();async function i(o,u={}){let a=o,h=u,H=t.getAll(),R=n.getAll(),m=s.getAll();if(H.length>0){let p=await ee(H,o,u);a=p.url,h=p.options}try{let p=await Z(a,h,r);return R.length>0?await re(R,p):p}catch(p){if(p instanceof l&&m.length>0){let y=await te(m,p);if(!(y instanceof l))return y;throw y}throw p}}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"})},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 O({...r,...o})},extend(o){return O({...r,...o})}}}var de=O(),ge=de;0&&(module.exports={HurlError,clearCache,createInstance});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var l=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 U(e){return new l({message:`HTTP ${e.status}: ${e.statusText}`,type:"HTTP_ERROR",...e})}function $(e,r){return new l({message:e,type:"NETWORK_ERROR",requestId:r})}function v(e,r){return new l({message:`Request timed out after ${e}ms`,type:"TIMEOUT_ERROR",requestId:r})}function F(e){return new l({message:"Request was aborted",type:"ABORT_ERROR",requestId:e})}function S(e,r){return new l({message:`Failed to parse response: ${e}`,type:"PARSE_ERROR",requestId:r})}async function B(e,r){let t=e.body?.getReader(),n=parseInt(e.headers.get("content-length")??"0",10);if(!t)return e.text();let o=[],i=0;for(;;){let{done:u,value:a}=await t.read();if(u)break;o.push(a),i+=a.length,r({loaded:i,total:n,percent:n>0?Math.round(i/n*100):0})}let c=new Uint8Array(i),s=0;for(let u of o)c.set(u,s),s+=u.length;return new TextDecoder().decode(c)}function O(e){let r={};return e.forEach((t,n)=>{r[n]=t}),r}async function _(e,r,t){let n=e.headers.get("content-type")??"";try{if(t){let o=await B(e,t);return n.includes("application/json")?JSON.parse(o):o}return n.includes("application/json")?await e.json():n.includes("text/")?await e.text():n.includes("application/octet-stream")||n.includes("image/")?await e.arrayBuffer():await e.text()}catch(o){throw S(o.message,r)}}function K(e,r,t,n){let o=Date.now();return{data:e,status:r.status,statusText:r.statusText,headers:O(r.headers),requestId:t,timing:{start:n,end:o,duration:o-n},fromCache:!1}}function L(e,r,t){if(t.type==="bearer"&&(e.Authorization=`Bearer ${t.token}`),t.type==="basic"){let n=btoa(`${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 M(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 N(e,r){let t=e.delay??300,n=e.backoff==="exponential"?t*Math.pow(2,r):t*(r+1);await new Promise(o=>setTimeout(o,n))}var x=new Map;function I(e,r){return r?.key??e}function G(e){let r=x.get(e);return r?Date.now()>r.expiresAt?(x.delete(e),null):{...r.response,fromCache:!0}:null}function W(e,r,t){x.set(e,{response:r,expiresAt:Date.now()+t.ttl})}function re(){x.clear()}var P=new Map;function z(e){return P.get(e)??null}function J(e,r){P.set(e,r),r.finally(()=>P.delete(e))}function Q(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 k(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 V(e){console.group("[hurl] \u{1F534} Error"),console.error(e),console.groupEnd()}function te(){return Math.random().toString(36).slice(2,10)}function ne(e,r,t){let n;if(r.startsWith("http://")||r.startsWith("https://")){if(e){let i=new URL(e).origin,c=new URL(r).origin;if(i!==c)throw new Error(`Absolute URL "${r}" does not match baseUrl origin "${i}". 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 o=new URLSearchParams;for(let[i,c]of Object.entries(t))o.set(i,String(c));return`${n}?${o.toString()}`}function se(e,r){let t={...r.headers,...e.headers},n=e.body;return n&&typeof n=="object"&&!(n instanceof FormData)&&!(n instanceof Blob)&&(t["Content-Type"]=t["Content-Type"]??"application/json"),t}function oe(e){if(e!=null)return e instanceof FormData||e instanceof Blob||e instanceof ArrayBuffer||typeof e=="string"?e:JSON.stringify(e)}async function X(e,r,t){let n=r.requestId??te(),o=r.method??"GET",i=Date.now(),c=M(r.retry??t.retry),s=r.debug??t.debug??!1,u={...t.query,...r.query},a=se(r,t),h=r.timeout??t.timeout,H=r.auth??t.auth;H&&L(a,u,H);let R=ne(t.baseUrl??"",e,Object.keys(u).length>0?u:void 0),m=r.cache??t.cache,p=!!m&&!m.bypass&&o==="GET";if(p){let d=I(R,m),T=G(d);if(T)return s&&k(T),T}let y=r.deduplicate??t.deduplicate??!1;if(y&&o==="GET"){let d=z(R);if(d)return d}s&&Q(R,{...r,method:o});let A=async d=>{let T=[],b=null,E=new AbortController;T.push(E),r.signal&&r.signal.addEventListener("abort",()=>E.abort()),h&&(b=setTimeout(()=>E.abort("timeout"),h));try{let f=await fetch(R,{method:o,headers:a,body:oe(r.body),signal:E.signal,redirect:r.followRedirects??!0?"follow":"manual"});b&&clearTimeout(b);let g=await _(f,n,r.onDownloadProgress);if(!f.ok)throw U({status:f.status,statusText:f.statusText,data:g,headers:O(f.headers),requestId:n,retries:d});let w=K(g,f,n,i);return p&&m&&W(I(R,m),w,m),s&&k(w),w}catch(f){b&&clearTimeout(b);let g;if(f instanceof l?g=f:f.name==="AbortError"?g=h&&f.message==="timeout"?v(h,n):F(n):g=$(f.message,n),g.retries=d,c&&j(g,c,d))return s&&console.log(`[hurl] retrying (${d+1}/${c.count})...`),await N(c,d),A(d+1);throw s&&V(g),g}},D=A(0);return y&&o==="GET"&&J(R,D),D}function q(){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 Y(e,r,t){let n={url:r,options:t};for(let o of e)n=await o(n.url,n.options);return n}async function Z(e,r){let t=r;for(let n of e)t=await n(t);return t}async function ee(e,r){let t=r;for(let n of e)t instanceof l&&(t=await n(t));return t}function C(e={}){let r={...e},t=q(),n=q(),o=q();async function i(s,u={}){let a=s,h=u,H=t.getAll(),R=n.getAll(),m=o.getAll();if(H.length>0){let p=await Y(H,s,u);a=p.url,h=p.options}try{let p=await X(a,h,r);return R.length>0?await Z(R,p):p}catch(p){if(p instanceof l&&m.length>0){let y=await ee(m,p);if(!(y instanceof l))return y;throw y}throw p}}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"})},all(s){return Promise.all(s)},defaults:{set(s){r={...r,...s}},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:o.use.bind(o),clear:o.clear.bind(o)}},create(s){return C({...r,...s})},extend(s){return C({...r,...s})}}}var ue=C(),Qe=ue;export{l as HurlError,re as clearCache,C as createInstance,Qe as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firekid/hurl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The modern HTTP client. Axios DX. Fetch speed. Zero dependencies.",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --minify --clean",
|
|
18
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["http", "https", "fetch", "axios", "request", "client", "typescript", "edge", "proxy", "retry"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"vitest": "^1.0.0",
|
|
29
|
+
"@types/node": "^20.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|