jiren 1.1.1 → 1.1.5

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/README.md CHANGED
@@ -6,12 +6,13 @@ Designed to be significantly faster than `fetch` and other Node/Bun HTTP clients
6
6
  ## Features
7
7
 
8
8
  - **Native Performance**: Written in Zig for memory safety and speed.
9
- - **Zero-Copy (Planned)**: Minimal overhead FFI.
10
- - **HTTP/1.1 & HTTP/3 (QUIC)**: Support for modern protocols.
9
+ - **Anti-Bot Protection**: Bypass Cloudflare with curl-impersonate (Chrome TLS fingerprinting).
10
+ - **HTTP/1.1, HTTP/2 & HTTP/3 (QUIC)**: Full support for modern protocols.
11
11
  - **Connection Pooling**: Reuse connections for maximum throughput.
12
- - **0-RTT Session Resumption**: Extremely fast subsequent requests.
13
- - **Smart Warmup**: Pre-warm connections to reduce latency.
12
+ - **Mandatory Warmup**: Pre-warm connections for instant requests.
13
+ - **Type-Safe URLs**: Define named endpoints with full TypeScript autocomplete.
14
14
  - **JSON & Text Helpers**: Easy response parsing.
15
+ - **Automatic Gzip Decompression**: Handles compressed responses transparently.
15
16
 
16
17
  ## Installation
17
18
 
@@ -21,17 +22,25 @@ bun add jiren
21
22
 
22
23
  ## Usage
23
24
 
24
- ### Basic GET
25
+ ### Basic Usage (Type-Safe URLs)
26
+
27
+ **Warmup is now mandatory** - you must define URLs in the constructor:
25
28
 
26
29
  ```typescript
27
30
  import { JirenClient } from "jiren";
28
31
 
29
- const client = new JirenClient();
30
- const res = await client.get("https://example.com");
32
+ const client = new JirenClient({
33
+ warmup: {
34
+ google: "https://www.google.com",
35
+ github: "https://api.github.com",
36
+ myapi: "https://api.myservice.com",
37
+ },
38
+ });
31
39
 
40
+ // TypeScript knows about 'google', 'github', 'myapi' - full autocomplete!
41
+ const res = await client.url.google.get();
32
42
  console.log(res.status);
33
- const text = await res.text();
34
- console.log(text);
43
+ const text = await res.body.text();
35
44
  ```
36
45
 
37
46
  ### JSON Response
@@ -42,38 +51,192 @@ interface User {
42
51
  name: string;
43
52
  }
44
53
 
45
- const res = await client.get<User>("https://api.example.com/user/1");
46
- const user = await res.json();
47
- console.log(user.name);
54
+ const client = new JirenClient({
55
+ warmup: {
56
+ api: "https://api.example.com",
57
+ },
58
+ });
59
+
60
+ const res = await client.url.api.get<User>({
61
+ path: "/user/1",
62
+ responseType: "json",
63
+ });
64
+ console.log(res.name); // Fully typed!
48
65
  ```
49
66
 
50
- ### Warmup / Prefetching
67
+ ### Anti-Bot Mode (NEW! �️)
68
+
69
+ Enable curl-impersonate for sites with bot protection:
70
+
71
+ ```typescript
72
+ const client = new JirenClient({
73
+ warmup: {
74
+ protected: "https://protected-site.com",
75
+ },
76
+ });
77
+
78
+ // Enable antibot mode for this request
79
+ const res = await client.url.protected.get({
80
+ antibot: true, // Uses curl-impersonate with Chrome fingerprinting
81
+ });
82
+ ```
51
83
 
52
- Warm up DNS and TLS handshakes to ensure subsequent requests are instant.
53
- You can do this in the constructor or via the `warmup` method.
84
+ ### POST, PUT, PATCH, DELETE
54
85
 
55
86
  ```typescript
56
- // Option 1: In constructor
57
87
  const client = new JirenClient({
58
- warmup: ["https://google.com", "https://cloudflare.com"],
88
+ warmup: {
89
+ api: "https://api.myservice.com",
90
+ },
59
91
  });
60
92
 
61
- // Option 2: Explicit method call
62
- client.warmup(["https://example.org"]);
93
+ // POST with body
94
+ const created = await client.url.api.post(
95
+ JSON.stringify({ name: "New Item" }),
96
+ {
97
+ path: "/items",
98
+ headers: { "Content-Type": "application/json" },
99
+ }
100
+ );
101
+
102
+ // PUT, PATCH, DELETE
103
+ await client.url.api.put(body, { path: "/items/1" });
104
+ await client.url.api.patch(body, { path: "/items/1" });
105
+ await client.url.api.delete(null, { path: "/items/1" });
106
+ ```
107
+
108
+ ### Response Helpers
109
+
110
+ ```typescript
111
+ const res = await client.url.api.get({ path: "/data" });
112
+
113
+ // Get as text
114
+ const text = await res.body.text();
115
+
116
+ // Get as JSON
117
+ const json = await res.body.json();
118
+
119
+ // Get as ArrayBuffer
120
+ const buffer = await res.body.arrayBuffer();
121
+
122
+ // Get as Blob
123
+ const blob = await res.body.blob();
63
124
 
64
- // ... later, requests are instant
65
- const res = await client.get("https://google.com");
125
+ // Or use responseType for automatic parsing
126
+ const data = await client.url.api.get({
127
+ path: "/data",
128
+ responseType: "json", // Returns parsed JSON directly
129
+ });
66
130
  ```
67
131
 
68
- ## Benchmarks
132
+ ## Why Jiren?
133
+
134
+ ### Comparison with Other Clients
135
+
136
+ | Feature | **Jiren** | **Axios** | **ky** | **got** | **node-fetch** |
137
+ | -------------------- | :-------: | :-------: | :----: | :-----: | :------------: |
138
+ | Type-safe named URLs | ✅ | ❌ | ❌ | ❌ | ❌ |
139
+ | Mandatory warmup | ✅ | ❌ | ❌ | ❌ | ❌ |
140
+ | HTTP/3 (QUIC) | ✅ | ❌ | ❌ | ❌ | ❌ |
141
+ | Anti-bot protection | ✅ | ❌ | ❌ | ❌ | ❌ |
142
+ | Native performance | ✅ | ❌ | ❌ | ❌ | ❌ |
143
+ | Zero code generation | ✅ | ✅ | ✅ | ✅ | ✅ |
144
+ | Bun FFI optimized | ✅ | ❌ | ❌ | ❌ | ❌ |
145
+
146
+ ### What Makes Jiren Unique
147
+
148
+ 1. **🔥 Mandatory Warmup** - Pre-establishes connections at startup for instant requests
149
+ 2. **📝 Type-Safe URLs** - Full autocomplete without needing backend schemas or code generation
150
+ 3. **🚀 Native Speed** - Zig-powered core bypasses JavaScript overhead
151
+ 4. **🛡️ Anti-Bot Protection** - Bypass Cloudflare, TLS fingerprinting, and bot detection with curl-impersonate
152
+ 5. **⚡ HTTP/3 Support** - First-class QUIC support for modern protocols
153
+
154
+ ## Anti-Bot Protection
155
+
156
+ The `antibot` option uses curl-impersonate to mimic Chrome's TLS fingerprint:
157
+
158
+ - Bypass TLS fingerprinting protections (JA3/JA4) like Cloudflare
159
+ - Chrome 120 browser impersonation
160
+ - Proper header ordering and HTTP/2 settings
161
+ - Automatic gzip decompression
162
+
163
+ ```typescript
164
+ const res = await client.url.site.get({
165
+ antibot: true, // Enable for protected sites
166
+ });
167
+ ```
168
+
169
+ **Performance**: ~1-2s for first request, faster with connection reuse.
170
+
171
+ ## API Reference
172
+
173
+ ### `JirenClient`
174
+
175
+ ```typescript
176
+ // Constructor options
177
+ interface JirenClientOptions {
178
+ warmup: Record<string, string>; // Required! Map of key -> URL
179
+ }
180
+
181
+ // Create client (warmup is mandatory)
182
+ const client = new JirenClient({
183
+ warmup: {
184
+ api: "https://api.example.com",
185
+ cdn: "https://cdn.example.com",
186
+ },
187
+ });
188
+
189
+ // Type-safe URL access
190
+ client.url[key].get(options?)
191
+ client.url[key].post(body?, options?)
192
+ client.url[key].put(body?, options?)
193
+ client.url[key].patch(body?, options?)
194
+ client.url[key].delete(body?, options?)
195
+ client.url[key].head(options?)
196
+ client.url[key].options(options?)
197
+
198
+ // Cleanup
199
+ client.close()
200
+ ```
201
+
202
+ ### Request Options
203
+
204
+ ```typescript
205
+ interface UrlRequestOptions {
206
+ path?: string; // Path to append to base URL
207
+ headers?: Record<string, string>;
208
+ maxRedirects?: number;
209
+ responseType?: "json" | "text" | "arraybuffer" | "blob";
210
+ antibot?: boolean; // Enable curl-impersonate (default: false)
211
+ }
212
+ ```
213
+
214
+ ### Response Object
215
+
216
+ ```typescript
217
+ interface JirenResponse<T> {
218
+ status: number;
219
+ statusText: string;
220
+ headers: Record<string, string>;
221
+ ok: boolean;
222
+ body: {
223
+ text(): Promise<string>;
224
+ json<R = T>(): Promise<R>;
225
+ arrayBuffer(): Promise<ArrayBuffer>;
226
+ blob(): Promise<Blob>;
227
+ };
228
+ }
229
+ ```
230
+
231
+ ## Performance
232
+
233
+ Benchmark results:
234
+
235
+ - **Bun fetch**: ~950-1780ms
236
+ - **JirenClient**: ~480-1630ms
69
237
 
70
- **Jiren** delivers native performance by bypassing the JavaScript engine overhead for network I/O.
238
+ JirenClient is often **faster than Bun's native fetch** thanks to optimized connection pooling and native Zig implementation.
71
239
 
72
- | Client | Requests/sec | Relative Speed |
73
- | ----------------- | ------------- | -------------- |
74
- | **Jiren** | **1,550,000** | **1.0x** |
75
- | Bun `fetch` | 45,000 | 34x Slower |
76
- | Node `http` | 32,000 | 48x Slower |
77
- | Python `requests` | 6,500 | 238x Slower |
240
+ ## License
78
241
 
79
- > Benchmarks run on MacBook Pro M3 Max, localhost loopback.
242
+ MIT
@@ -1,23 +1,258 @@
1
1
  import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
2
2
  import { lib } from "./native";
3
- import type { RequestOptions } from "./types";
3
+ import type {
4
+ RequestOptions,
5
+ JirenResponse,
6
+ JirenResponseBody,
7
+ WarmupUrlConfig,
8
+ UrlRequestOptions,
9
+ UrlEndpoint,
10
+ } from "./types";
4
11
 
5
- export interface JirenClientOptions {
12
+ const STATUS_TEXT: Record<number, string> = {
13
+ 200: "OK",
14
+ 201: "Created",
15
+ 204: "No Content",
16
+ 301: "Moved Permanently",
17
+ 302: "Found",
18
+ 400: "Bad Request",
19
+ 401: "Unauthorized",
20
+ 403: "Forbidden",
21
+ 404: "Not Found",
22
+ 500: "Internal Server Error",
23
+ 502: "Bad Gateway",
24
+ 503: "Service Unavailable",
25
+ };
26
+
27
+ /** Options for JirenClient constructor */
28
+ export interface JirenClientOptions<
29
+ T extends readonly WarmupUrlConfig[] | Record<string, string> =
30
+ | readonly WarmupUrlConfig[]
31
+ | Record<string, string>
32
+ > {
6
33
  /** URLs to warmup on client creation (pre-connect + handshake) */
7
- warmup?: string[];
34
+ warmup?: string[] | T;
35
+
36
+ /** Enable benchmark mode (Force HTTP/2, disable probing) */
37
+ benchmark?: boolean;
8
38
  }
9
39
 
10
- export class JirenClient {
40
+ /** Helper to extract keys from Warmup Config */
41
+ export type ExtractWarmupKeys<
42
+ T extends readonly WarmupUrlConfig[] | Record<string, string>
43
+ > = T extends readonly WarmupUrlConfig[]
44
+ ? T[number]["key"]
45
+ : T extends Record<string, string>
46
+ ? keyof T
47
+ : never;
48
+
49
+ /** Type-safe URL accessor - maps keys to UrlEndpoint objects */
50
+ export type UrlAccessor<
51
+ T extends readonly WarmupUrlConfig[] | Record<string, string>
52
+ > = {
53
+ [K in ExtractWarmupKeys<T>]: UrlEndpoint;
54
+ };
55
+
56
+ /**
57
+ * Helper function to define warmup URLs with type inference.
58
+ * This eliminates the need for 'as const'.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const client = new JirenClient({
63
+ * warmup: defineUrls([
64
+ * { key: "google", url: "https://google.com" },
65
+ * ])
66
+ * });
67
+ * // OR
68
+ * const client = new JirenClient({
69
+ * warmup: {
70
+ * google: "https://google.com"
71
+ * }
72
+ * });
73
+ * ```
74
+ */
75
+ export function defineUrls<const T extends readonly WarmupUrlConfig[]>(
76
+ urls: T
77
+ ): T {
78
+ return urls;
79
+ }
80
+
81
+ export class JirenClient<
82
+ T extends readonly WarmupUrlConfig[] | Record<string, string> =
83
+ | readonly WarmupUrlConfig[]
84
+ | Record<string, string>
85
+ > {
11
86
  private ptr: Pointer | null;
87
+ private urlMap: Map<string, string> = new Map();
88
+
89
+ /** Type-safe URL accessor for warmed-up URLs */
90
+ public readonly url: UrlAccessor<T>;
12
91
 
13
- constructor(options?: JirenClientOptions) {
92
+ constructor(options?: JirenClientOptions<T>) {
14
93
  this.ptr = lib.symbols.zclient_new();
15
94
  if (!this.ptr) throw new Error("Failed to create native client instance");
16
95
 
17
- // Warmup connections immediately if URLs provided
18
- if (options?.warmup && options.warmup.length > 0) {
19
- this.warmup(options.warmup);
96
+ // Enable benchmark mode if requested
97
+ if (options?.benchmark) {
98
+ lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
99
+ }
100
+
101
+ // Process warmup URLs
102
+ if (options?.warmup) {
103
+ const urls: string[] = [];
104
+ const warmup = options.warmup;
105
+
106
+ if (Array.isArray(warmup)) {
107
+ for (const item of warmup) {
108
+ if (typeof item === "string") {
109
+ urls.push(item);
110
+ } else {
111
+ // WarmupUrlConfig with key
112
+ const config = item as WarmupUrlConfig;
113
+ urls.push(config.url);
114
+ this.urlMap.set(config.key, config.url);
115
+ }
116
+ }
117
+ } else {
118
+ // Record<string, string>
119
+ for (const [key, url] of Object.entries(warmup)) {
120
+ urls.push(url as string);
121
+ this.urlMap.set(key, url as string);
122
+ }
123
+ }
124
+
125
+ if (urls.length > 0) {
126
+ this.warmup(urls);
127
+ }
20
128
  }
129
+
130
+ // Create proxy for type-safe URL access
131
+ this.url = this.createUrlAccessor();
132
+ }
133
+
134
+ /**
135
+ * Creates a proxy-based URL accessor for type-safe access to warmed-up URLs.
136
+ */
137
+ private createUrlAccessor(): UrlAccessor<T> {
138
+ const self = this;
139
+
140
+ return new Proxy({} as UrlAccessor<T>, {
141
+ get(_target, prop: string) {
142
+ const baseUrl = self.urlMap.get(prop);
143
+ if (!baseUrl) {
144
+ throw new Error(
145
+ `URL key "${prop}" not found. Available keys: ${Array.from(
146
+ self.urlMap.keys()
147
+ ).join(", ")}`
148
+ );
149
+ }
150
+
151
+ // Helper to build full URL with optional path
152
+ const buildUrl = (path?: string) =>
153
+ path
154
+ ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
155
+ : baseUrl;
156
+
157
+ // Return a UrlEndpoint object with all HTTP methods
158
+ return {
159
+ get: async <R = any>(
160
+ options?: UrlRequestOptions
161
+ ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
162
+ return self.request<R>("GET", buildUrl(options?.path), null, {
163
+ headers: options?.headers,
164
+ maxRedirects: options?.maxRedirects,
165
+ responseType: options?.responseType,
166
+ antibot: options?.antibot,
167
+ });
168
+ },
169
+
170
+ post: async <R = any>(
171
+ body?: string | null,
172
+ options?: UrlRequestOptions
173
+ ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
174
+ return self.request<R>(
175
+ "POST",
176
+ buildUrl(options?.path),
177
+ body || null,
178
+ {
179
+ headers: options?.headers,
180
+ maxRedirects: options?.maxRedirects,
181
+ responseType: options?.responseType,
182
+ }
183
+ );
184
+ },
185
+
186
+ put: async <R = any>(
187
+ body?: string | null,
188
+ options?: UrlRequestOptions
189
+ ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
190
+ return self.request<R>(
191
+ "PUT",
192
+ buildUrl(options?.path),
193
+ body || null,
194
+ {
195
+ headers: options?.headers,
196
+ maxRedirects: options?.maxRedirects,
197
+ responseType: options?.responseType,
198
+ }
199
+ );
200
+ },
201
+
202
+ patch: async <R = any>(
203
+ body?: string | null,
204
+ options?: UrlRequestOptions
205
+ ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
206
+ return self.request<R>(
207
+ "PATCH",
208
+ buildUrl(options?.path),
209
+ body || null,
210
+ {
211
+ headers: options?.headers,
212
+ maxRedirects: options?.maxRedirects,
213
+ responseType: options?.responseType,
214
+ }
215
+ );
216
+ },
217
+
218
+ delete: async <R = any>(
219
+ body?: string | null,
220
+ options?: UrlRequestOptions
221
+ ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
222
+ return self.request<R>(
223
+ "DELETE",
224
+ buildUrl(options?.path),
225
+ body || null,
226
+ {
227
+ headers: options?.headers,
228
+ maxRedirects: options?.maxRedirects,
229
+ responseType: options?.responseType,
230
+ }
231
+ );
232
+ },
233
+
234
+ head: async (
235
+ options?: UrlRequestOptions
236
+ ): Promise<JirenResponse<any>> => {
237
+ return self.request("HEAD", buildUrl(options?.path), null, {
238
+ headers: options?.headers,
239
+ maxRedirects: options?.maxRedirects,
240
+ antibot: options?.antibot,
241
+ });
242
+ },
243
+
244
+ options: async (
245
+ options?: UrlRequestOptions
246
+ ): Promise<JirenResponse<any>> => {
247
+ return self.request("OPTIONS", buildUrl(options?.path), null, {
248
+ headers: options?.headers,
249
+ maxRedirects: options?.maxRedirects,
250
+ antibot: options?.antibot,
251
+ });
252
+ },
253
+ } as UrlEndpoint;
254
+ },
255
+ });
21
256
  }
22
257
 
23
258
  /**
@@ -58,27 +293,67 @@ export class JirenClient {
58
293
  * @param url - The URL to request
59
294
  * @param body - The body content string (optional)
60
295
  * @param options - Request options (headers, maxRedirects, etc.) or just headers map
61
- * @returns Promise resolving to Response object
296
+ * @returns Promise resolving to Response object or parsed body
62
297
  */
298
+ public async request<T = any>(
299
+ method: string,
300
+ url: string,
301
+ body?: string | null,
302
+ options?: RequestOptions & { responseType: "json" }
303
+ ): Promise<T>;
304
+ public async request<T = any>(
305
+ method: string,
306
+ url: string,
307
+ body?: string | null,
308
+ options?: RequestOptions & { responseType: "text" }
309
+ ): Promise<string>;
310
+ public async request<T = any>(
311
+ method: string,
312
+ url: string,
313
+ body?: string | null,
314
+ options?: RequestOptions & { responseType: "arraybuffer" }
315
+ ): Promise<ArrayBuffer>;
316
+ public async request<T = any>(
317
+ method: string,
318
+ url: string,
319
+ body?: string | null,
320
+ options?: RequestOptions & { responseType: "blob" }
321
+ ): Promise<Blob>;
322
+ public async request<T = any>(
323
+ method: string,
324
+ url: string,
325
+ body?: string | null,
326
+ options?: RequestOptions
327
+ ): Promise<JirenResponse<T>>;
63
328
  public async request<T = any>(
64
329
  method: string,
65
330
  url: string,
66
331
  body?: string | null,
67
332
  options?: RequestOptions | Record<string, string> | null
68
- ) {
333
+ ): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
69
334
  if (!this.ptr) throw new Error("Client is closed");
70
335
 
71
336
  // Normalize options
72
337
  let headers: Record<string, string> = {};
73
338
  let maxRedirects = 5; // Default
339
+ let responseType: RequestOptions["responseType"] | undefined;
340
+ let antibot = false; // Default
74
341
 
75
342
  if (options) {
76
- if ("maxRedirects" in options || "headers" in options) {
343
+ if (
344
+ "maxRedirects" in options ||
345
+ "headers" in options ||
346
+ "responseType" in options ||
347
+ "method" in options || // Check for any RequestOptions specific key
348
+ "timeout" in options ||
349
+ "antibot" in options
350
+ ) {
77
351
  // It is RequestOptions
78
352
  const opts = options as RequestOptions;
79
353
  if (opts.headers) headers = opts.headers;
80
354
  if (opts.maxRedirects !== undefined) maxRedirects = opts.maxRedirects;
81
- // Merge top-level unknown keys as headers if lenient? No, strict to types.
355
+ if (opts.responseType) responseType = opts.responseType;
356
+ if (opts.antibot !== undefined) antibot = opts.antibot;
82
357
  } else {
83
358
  // Assume it's just headers Record<string, string> for backward compatibility
84
359
  headers = options as Record<string, string>;
@@ -99,13 +374,52 @@ export class JirenClient {
99
374
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
100
375
  accept:
101
376
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
102
- "accept-encoding": "gzip, deflate, br",
377
+ "accept-encoding": "gzip",
103
378
  "accept-language": "en-US,en;q=0.9",
379
+ "sec-ch-ua":
380
+ '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
381
+ "sec-ch-ua-mobile": "?0",
382
+ "sec-ch-ua-platform": '"macOS"',
383
+ "sec-fetch-dest": "document",
384
+ "sec-fetch-mode": "navigate",
385
+ "sec-fetch-site": "none",
386
+ "sec-fetch-user": "?1",
387
+ "upgrade-insecure-requests": "1",
104
388
  };
105
389
 
106
390
  const finalHeaders = { ...defaultHeaders, ...headers };
107
391
 
108
- const headerStr = Object.entries(finalHeaders)
392
+ // Enforce Chrome header order
393
+ const orderedHeaders: Record<string, string> = {};
394
+ const keys = [
395
+ "sec-ch-ua",
396
+ "sec-ch-ua-mobile",
397
+ "sec-ch-ua-platform",
398
+ "upgrade-insecure-requests",
399
+ "user-agent",
400
+ "accept",
401
+ "sec-fetch-site",
402
+ "sec-fetch-mode",
403
+ "sec-fetch-user",
404
+ "sec-fetch-dest",
405
+ "accept-encoding",
406
+ "accept-language",
407
+ ];
408
+
409
+ // Add priority headers in order
410
+ for (const key of keys) {
411
+ if (finalHeaders[key]) {
412
+ orderedHeaders[key] = finalHeaders[key];
413
+ delete finalHeaders[key];
414
+ }
415
+ }
416
+
417
+ // Add remaining custom headers
418
+ for (const [key, value] of Object.entries(finalHeaders)) {
419
+ orderedHeaders[key] = value;
420
+ }
421
+
422
+ const headerStr = Object.entries(orderedHeaders)
109
423
  .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
110
424
  .join("\r\n");
111
425
 
@@ -119,60 +433,27 @@ export class JirenClient {
119
433
  urlBuffer,
120
434
  headersBuffer,
121
435
  bodyBuffer,
122
- maxRedirects
436
+ maxRedirects,
437
+ antibot
123
438
  );
124
439
 
125
- return this.parseResponse<T>(respPtr);
126
- }
127
-
128
- public async get<T = any>(
129
- url: string,
130
- options?: RequestOptions | Record<string, string>
131
- ) {
132
- return this.request<T>("GET", url, null, options);
133
- }
134
-
135
- public async post<T = any>(
136
- url: string,
137
- body: string,
138
- options?: RequestOptions | Record<string, string>
139
- ) {
140
- return this.request<T>("POST", url, body, options);
141
- }
142
-
143
- public async put<T = any>(
144
- url: string,
145
- body: string,
146
- options?: RequestOptions | Record<string, string>
147
- ) {
148
- return this.request<T>("PUT", url, body, options);
149
- }
150
-
151
- public async patch<T = any>(
152
- url: string,
153
- body: string,
154
- options?: RequestOptions | Record<string, string>
155
- ) {
156
- return this.request<T>("PATCH", url, body, options);
157
- }
158
-
159
- public async delete<T = any>(
160
- url: string,
161
- body?: string,
162
- options?: RequestOptions | Record<string, string>
163
- ) {
164
- return this.request<T>("DELETE", url, body || null, options);
165
- }
440
+ const response = this.parseResponse<T>(respPtr, url);
166
441
 
167
- public async head(url: string, headers?: Record<string, string>) {
168
- return this.request("HEAD", url, null, headers);
169
- }
442
+ // Auto-parse if requested
443
+ if (responseType) {
444
+ if (responseType === "json") return response.body.json();
445
+ if (responseType === "text") return response.body.text();
446
+ if (responseType === "arraybuffer") return response.body.arrayBuffer();
447
+ if (responseType === "blob") return response.body.blob();
448
+ }
170
449
 
171
- public async options(url: string, headers?: Record<string, string>) {
172
- return this.request("OPTIONS", url, null, headers);
450
+ return response;
173
451
  }
174
452
 
175
- private parseResponse<T = any>(respPtr: Pointer | null) {
453
+ private parseResponse<T = any>(
454
+ respPtr: Pointer | null,
455
+ url: string
456
+ ): JirenResponse<T> {
176
457
  if (!respPtr)
177
458
  throw new Error("Native request failed (returned null pointer)");
178
459
 
@@ -181,20 +462,178 @@ export class JirenClient {
181
462
  const len = Number(lib.symbols.zclient_response_body_len(respPtr));
182
463
  const bodyPtr = lib.symbols.zclient_response_body(respPtr);
183
464
 
184
- let bodyString = "";
465
+ const headersLen = Number(
466
+ lib.symbols.zclient_response_headers_len(respPtr)
467
+ );
468
+ let headersObj: Record<string, string> | NativeHeaders = {};
469
+
470
+ if (headersLen > 0) {
471
+ const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
472
+ if (rawHeadersPtr) {
473
+ // Copy headers to JS memory
474
+ // We need to copy because respPtr will be freed
475
+ const rawSrc = toArrayBuffer(rawHeadersPtr, 0, headersLen);
476
+ const raw = new Uint8Array(rawSrc.slice(0)); // Explicit copy
477
+
478
+ headersObj = new NativeHeaders(raw);
479
+ }
480
+ }
481
+
482
+ // Proxy for backward compatibility
483
+ const headersProxy = new Proxy(
484
+ headersObj instanceof NativeHeaders ? headersObj : {},
485
+ {
486
+ get(target, prop) {
487
+ if (target instanceof NativeHeaders && typeof prop === "string") {
488
+ if (prop === "toJSON") return () => target.toJSON();
489
+
490
+ // Try to get from native headers
491
+ const val = target.get(prop);
492
+ if (val !== null) return val;
493
+ }
494
+ return Reflect.get(target, prop);
495
+ },
496
+ }
497
+ ) as unknown as Record<string, string>; // Lie to TS
498
+
499
+ let buffer: ArrayBuffer | Buffer = new ArrayBuffer(0);
185
500
  if (len > 0 && bodyPtr) {
186
- const buffer = toArrayBuffer(bodyPtr, 0, len);
187
- bodyString = new TextDecoder().decode(buffer);
501
+ // Create a copy of the buffer because the native response is freed immediately after
502
+ buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
188
503
  }
189
504
 
505
+ let bodyUsed = false;
506
+ const consumeBody = () => {
507
+ if (bodyUsed) {
508
+ }
509
+ bodyUsed = true;
510
+ };
511
+
512
+ const bodyObj: JirenResponseBody<T> = {
513
+ bodyUsed: false,
514
+ arrayBuffer: async () => {
515
+ consumeBody();
516
+ if (Buffer.isBuffer(buffer)) {
517
+ const buf = buffer as Buffer;
518
+ return buf.buffer.slice(
519
+ buf.byteOffset,
520
+ buf.byteOffset + buf.byteLength
521
+ ) as ArrayBuffer;
522
+ }
523
+ return buffer as ArrayBuffer;
524
+ },
525
+ blob: async () => {
526
+ consumeBody();
527
+ return new Blob([buffer]);
528
+ },
529
+ text: async () => {
530
+ consumeBody();
531
+ return new TextDecoder().decode(buffer);
532
+ },
533
+ json: async <R = T>(): Promise<R> => {
534
+ consumeBody();
535
+ const text = new TextDecoder().decode(buffer);
536
+ return JSON.parse(text);
537
+ },
538
+ };
539
+
540
+ // Update bodyUsed getter to reflect local variable
541
+ Object.defineProperty(bodyObj, "bodyUsed", {
542
+ get: () => bodyUsed,
543
+ });
544
+
190
545
  return {
546
+ url,
191
547
  status,
192
- body: bodyString,
193
- text: async () => bodyString,
194
- json: async (): Promise<T> => JSON.parse(bodyString),
195
- };
548
+ statusText: STATUS_TEXT[status] || "",
549
+ headers: headersProxy,
550
+ ok: status >= 200 && status < 300,
551
+ redirected: false,
552
+ type: "basic",
553
+ body: bodyObj,
554
+ } as JirenResponse<T>;
196
555
  } finally {
197
556
  lib.symbols.zclient_response_free(respPtr);
198
557
  }
199
558
  }
200
559
  }
560
+
561
+ class NativeHeaders {
562
+ private raw: Uint8Array;
563
+ private len: number;
564
+ private decoder = new TextDecoder();
565
+ private cache = new Map<string, string>();
566
+ // We need a pointer to the raw buffer for FFI calls.
567
+ // Since we can't easily rely on ptr(this.raw) being stable if we stored it,
568
+ // we will pass this.raw to the FFI call directly each time.
569
+
570
+ constructor(raw: Uint8Array) {
571
+ this.raw = raw;
572
+ this.len = raw.byteLength;
573
+ }
574
+
575
+ get(name: string): string | null {
576
+ const target = name.toLowerCase();
577
+ if (this.cache.has(target)) return this.cache.get(target)!;
578
+
579
+ const keyBuf = Buffer.from(target + "\0");
580
+
581
+ // Debug log
582
+ // Pass the raw buffer directly. Bun handles the pointer.
583
+ const resPtr = lib.symbols.z_find_header_value(
584
+ this.raw as any,
585
+ this.len,
586
+ keyBuf
587
+ );
588
+
589
+ if (!resPtr) return null;
590
+
591
+ try {
592
+ // ZHeaderValue: { value_ptr: pointer, value_len: size_t }
593
+ // Assuming 64-bit architecture, pointers and size_t are 8 bytes.
594
+ // Struct size = 16 bytes.
595
+ const view = new DataView(toArrayBuffer(resPtr, 0, 16));
596
+ const valPtr = view.getBigUint64(0, true);
597
+ const valLen = Number(view.getBigUint64(8, true));
598
+
599
+ if (valLen === 0) {
600
+ this.cache.set(target, "");
601
+ return "";
602
+ }
603
+
604
+ // Convert valPtr to ArrayBuffer
605
+ // Note: valPtr points inside this.raw, but toArrayBuffer(ptr) creates a view on that memory.
606
+ const valBytes = toArrayBuffer(Number(valPtr) as any, 0, valLen);
607
+ const val = this.decoder.decode(valBytes);
608
+
609
+ this.cache.set(target, val);
610
+ return val;
611
+ } finally {
612
+ lib.symbols.zclient_header_value_free(resPtr);
613
+ }
614
+ }
615
+
616
+ // Fallback for when full object is needed (e.g. debugging)
617
+ // This is expensive as it reparses everything using the old offset method
618
+ // BUT we don't have the offset method easily available on the raw buffer unless we expose a new one?
619
+ // Wait, `zclient_response_parse_header_offsets` takes `Response*`.
620
+ // We don't have Response* anymore.
621
+ // We need `z_parse_header_offsets_from_raw(ptr, len)`.
622
+ // Or just parse in JS since we have the full buffer?
623
+ // Actually, we can just do a JS parser since we have the buffer.
624
+ // It's a fallback anyway.
625
+ toJSON(): Record<string, string> {
626
+ const obj: Record<string, string> = {};
627
+ const text = this.decoder.decode(this.raw);
628
+ const lines = text.split("\r\n");
629
+ for (const line of lines) {
630
+ if (!line) continue;
631
+ const colon = line.indexOf(":");
632
+ if (colon === -1) continue;
633
+ const key = line.substring(0, colon).trim().toLowerCase();
634
+ const val = line.substring(colon + 1).trim();
635
+ obj[key] = val;
636
+ }
637
+ return obj;
638
+ }
639
+ }
@@ -5,9 +5,19 @@
5
5
  */
6
6
 
7
7
  // Main client
8
- export { JirenClient, type JirenClientOptions } from "./client";
8
+ export {
9
+ JirenClient,
10
+ type JirenClientOptions,
11
+ type UrlAccessor,
12
+ } from "./client";
9
13
 
10
14
  // Types
11
- export type { JirenHttpConfig, ParsedUrl } from "../types/index";
15
+ export type {
16
+ JirenHttpConfig,
17
+ ParsedUrl,
18
+ WarmupUrlConfig,
19
+ UrlRequestOptions,
20
+ UrlEndpoint,
21
+ } from "./types";
12
22
 
13
23
  // Remove broken exports
@@ -29,6 +29,7 @@ export const ffiDef = {
29
29
  FFIType.cstring,
30
30
  FFIType.cstring,
31
31
  FFIType.u8,
32
+ FFIType.bool,
32
33
  ],
33
34
  returns: FFIType.ptr,
34
35
  },
@@ -56,10 +57,30 @@ export const ffiDef = {
56
57
  args: [FFIType.ptr],
57
58
  returns: FFIType.u64,
58
59
  },
60
+ zclient_response_parse_header_offsets: {
61
+ args: [FFIType.ptr],
62
+ returns: FFIType.ptr,
63
+ },
64
+ zclient_header_offsets_free: {
65
+ args: [FFIType.ptr],
66
+ returns: FFIType.void,
67
+ },
68
+ z_find_header_value: {
69
+ args: [FFIType.ptr, FFIType.u64, FFIType.cstring],
70
+ returns: FFIType.ptr,
71
+ },
72
+ zclient_header_value_free: {
73
+ args: [FFIType.ptr],
74
+ returns: FFIType.void,
75
+ },
59
76
  zclient_response_free: {
60
77
  args: [FFIType.ptr],
61
78
  returns: FFIType.void,
62
79
  },
80
+ zclient_set_benchmark_mode: {
81
+ args: [FFIType.ptr, FFIType.bool],
82
+ returns: FFIType.void,
83
+ },
63
84
  } as const;
64
85
 
65
86
  export const lib = dlopen(libPath, ffiDef);
@@ -34,16 +34,44 @@ export interface RequestOptions {
34
34
  timeout?: number;
35
35
  /** Maximum number of redirects to follow */
36
36
  maxRedirects?: number;
37
+ /** Automatically parse response body */
38
+ responseType?: "json" | "text" | "arraybuffer" | "blob";
39
+ /** Enable anti-bot protection (using curl-impersonate) */
40
+ antibot?: boolean;
41
+ }
42
+
43
+ /** HTTP Response Body Wrapper */
44
+ export interface JirenResponseBody<T = any> {
45
+ /** Get response as JSON */
46
+ json: <R = T>() => Promise<R>;
47
+ /** Get response as text */
48
+ text: () => Promise<string>;
49
+ /** Get response as ArrayBuffer */
50
+ arrayBuffer: () => Promise<ArrayBuffer>;
51
+ /** Get response as Blob */
52
+ blob: () => Promise<Blob>;
53
+ /** Whether the body has been used */
54
+ bodyUsed: boolean;
37
55
  }
38
56
 
39
57
  /** HTTP Response */
40
- export interface HttpResponse {
58
+ export interface JirenResponse<T = any> {
41
59
  /** HTTP status code */
42
60
  status: number;
43
- /** Response body as string */
44
- body: string;
61
+ /** Response body object */
62
+ body: JirenResponseBody<T>;
45
63
  /** Response headers */
46
- headers?: Record<string, string>;
64
+ headers: Record<string, string>;
65
+ /** Whether the request was successful (status 200-299) */
66
+ ok: boolean;
67
+ /** The URL of the response */
68
+ url: string;
69
+ /** The status message corresponding to the status code */
70
+ statusText: string;
71
+ /** Whether the response was redirected */
72
+ redirected: boolean;
73
+ /** The type of the response */
74
+ type: "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect";
47
75
  }
48
76
 
49
77
  /** Configuration for JirenHttpClient */
@@ -61,3 +89,87 @@ export interface ParsedUrl {
61
89
  port: number;
62
90
  path: string;
63
91
  }
92
+
93
+ /** Warmup URL with a key for type-safe access */
94
+ export interface WarmupUrlConfig {
95
+ /** Unique key to access this URL via client.url[key] */
96
+ key: string;
97
+ /** The URL to warmup */
98
+ url: string;
99
+ }
100
+
101
+ /** Options for URL endpoint requests */
102
+ export interface UrlRequestOptions {
103
+ /** Request headers */
104
+ headers?: Record<string, string>;
105
+ /** Path to append to the base URL */
106
+ path?: string;
107
+ /** Maximum number of redirects to follow */
108
+ maxRedirects?: number;
109
+ /** Automatically parse response body */
110
+ responseType?: "json" | "text" | "arraybuffer" | "blob";
111
+ /** Enable anti-bot protection (using curl-impersonate) */
112
+ antibot?: boolean;
113
+ }
114
+
115
+ /** Interface for a URL endpoint with HTTP method helpers */
116
+ export interface UrlEndpoint {
117
+ /** GET request */
118
+ get<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
119
+ get<T = any>(
120
+ options?: UrlRequestOptions & { responseType: "json" }
121
+ ): Promise<T>;
122
+ get<T = any>(
123
+ options?: UrlRequestOptions & { responseType: "text" }
124
+ ): Promise<string>;
125
+
126
+ /** POST request */
127
+ post<T = any>(
128
+ body?: string | null,
129
+ options?: UrlRequestOptions
130
+ ): Promise<JirenResponse<T>>;
131
+ post<T = any>(
132
+ body?: string | null,
133
+ options?: UrlRequestOptions & { responseType: "json" }
134
+ ): Promise<T>;
135
+
136
+ /** PUT request */
137
+ put<T = any>(
138
+ body?: string | null,
139
+ options?: UrlRequestOptions
140
+ ): Promise<JirenResponse<T>>;
141
+ put<T = any>(
142
+ body?: string | null,
143
+ options?: UrlRequestOptions & { responseType: "json" }
144
+ ): Promise<T>;
145
+
146
+ /** PATCH request */
147
+ patch<T = any>(
148
+ body?: string | null,
149
+ options?: UrlRequestOptions
150
+ ): Promise<JirenResponse<T>>;
151
+ patch<T = any>(
152
+ body?: string | null,
153
+ options?: UrlRequestOptions & { responseType: "json" }
154
+ ): Promise<T>;
155
+
156
+ /** DELETE request */
157
+ delete<T = any>(
158
+ body?: string | null,
159
+ options?: UrlRequestOptions
160
+ ): Promise<JirenResponse<T>>;
161
+ delete<T = any>(
162
+ body?: string | null,
163
+ options?: UrlRequestOptions & { responseType: "json" }
164
+ ): Promise<T>;
165
+
166
+ /** HEAD request */
167
+ head(options?: UrlRequestOptions): Promise<JirenResponse<any>>;
168
+
169
+ /** OPTIONS request */
170
+ options(options?: UrlRequestOptions): Promise<JirenResponse<any>>;
171
+ }
172
+
173
+ /** Type helper to extract keys from warmup config array */
174
+ export type ExtractWarmupKeys<T extends readonly WarmupUrlConfig[]> =
175
+ T[number]["key"];
@@ -75,12 +75,17 @@ if (parentPort) {
75
75
  bodyBuffer = Buffer.from(bodyStr + "\0");
76
76
  }
77
77
 
78
+ const maxRedirects = options.maxRedirects ?? 5;
79
+ const antibot = options.antibot ?? false;
80
+
78
81
  const respPtr = lib.symbols.zclient_request(
79
82
  ptr,
80
83
  methodBuffer,
81
84
  urlBuffer,
82
85
  headersBuffer,
83
- bodyBuffer
86
+ bodyBuffer,
87
+ maxRedirects,
88
+ antibot
84
89
  );
85
90
 
86
91
  // Parse Response
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.1.1",
3
+ "version": "1.1.5",
4
4
  "author": "",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",
package/types/index.ts CHANGED
@@ -33,6 +33,12 @@ export interface RequestOptions {
33
33
  body?: string | object;
34
34
  /** Request timeout in milliseconds */
35
35
  timeout?: number;
36
+ /** Maximum number of redirects to follow */
37
+ maxRedirects?: number;
38
+ /** Automatically parse response body */
39
+ responseType?: "json" | "text" | "arraybuffer" | "blob";
40
+ /** Enable anti-bot protection (using curl-impersonate) */
41
+ antibot?: boolean;
36
42
  }
37
43
 
38
44
  /** HTTP Response */