@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 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
@@ -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 };
@@ -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
+ }