jiren 1.0.15 → 1.1.1

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
@@ -1,19 +1,17 @@
1
- # jiren
1
+ # Jiren
2
2
 
3
- > **⚠️ BUN ONLY**: This package requires [Bun](https://bun.sh) runtime. Node.js is not supported.
4
-
5
- Ultra-fast HTTP/HTTPS client.
3
+ Ultra-fast HTTP/HTTPS client powered by native Zig (FFI).
6
4
  Designed to be significantly faster than `fetch` and other Node/Bun HTTP clients.
7
5
 
8
6
  ## Features
9
7
 
10
- - **Blazing Fast**: Native implementation with minimal overhead.
11
- - **HTTP/3 (QUIC) Support**: First-class support for modern, high-performance connections.
12
- - **Automatic Redirects**: Recursively follows redirects (301, 302, etc.) with cycle protection.
13
- - **Type-Safe API**: Strict options (`GetRequestOptions`, `PostRequestOptions`) for better DX.
14
- - **Helper Methods**: Built-in `.json<T>()` parser and header accessors.
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.
15
11
  - **Connection Pooling**: Reuse connections for maximum throughput.
16
12
  - **0-RTT Session Resumption**: Extremely fast subsequent requests.
13
+ - **Smart Warmup**: Pre-warm connections to reduce latency.
14
+ - **JSON & Text Helpers**: Easy response parsing.
17
15
 
18
16
  ## Installation
19
17
 
@@ -23,65 +21,57 @@ bun add jiren
23
21
 
24
22
  ## Usage
25
23
 
26
- ### Basic Requests
24
+ ### Basic GET
27
25
 
28
26
  ```typescript
29
27
  import { JirenClient } from "jiren";
30
28
 
31
29
  const client = new JirenClient();
30
+ const res = await client.get("https://example.com");
32
31
 
33
- // Simple GET
34
- const res = client.get("https://google.com");
35
- console.log(res.status); // 200
36
-
37
- // JSON Parsing Helper
38
- const data = res.json<{ message: string }>();
32
+ console.log(res.status);
33
+ const text = await res.text();
34
+ console.log(text);
39
35
  ```
40
36
 
41
- ### Advanced Options (Headers, Redirects, Body)
42
-
43
- Jiren enforces type safety. You cannot pass a body to a GET request!
37
+ ### JSON Response
44
38
 
45
39
  ```typescript
46
- // GET with Headers
47
- client.get("https://api.example.com", {
48
- headers: { Authorization: "Bearer token" },
49
- });
50
-
51
- // POST with JSON body
52
- client.post(
53
- "https://api.example.com/users",
54
- JSON.stringify({ name: "Jiren" }),
55
- {
56
- headers: { "Content-Type": "application/json" },
57
- }
58
- );
59
-
60
- // Follow Redirects (e.g., http -> https)
61
- client.get("http://google.com", {
62
- maxRedirects: 5, // Automatically follows up to 5 hops
63
- });
40
+ interface User {
41
+ id: number;
42
+ name: string;
43
+ }
44
+
45
+ const res = await client.get<User>("https://api.example.com/user/1");
46
+ const user = await res.json();
47
+ console.log(user.name);
64
48
  ```
65
49
 
66
- ### Prefetching
50
+ ### Warmup / Prefetching
67
51
 
68
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.
69
54
 
70
55
  ```typescript
71
- // Prefetch connection (DNS + TLS Handshake)
72
- client.prefetch(["https://google.com", "https://cloudflare.com"]);
56
+ // Option 1: In constructor
57
+ const client = new JirenClient({
58
+ warmup: ["https://google.com", "https://cloudflare.com"],
59
+ });
60
+
61
+ // Option 2: Explicit method call
62
+ client.warmup(["https://example.org"]);
73
63
 
74
64
  // ... later, requests are instant
75
- const res = client.get("https://google.com");
65
+ const res = await client.get("https://google.com");
76
66
  ```
77
67
 
78
68
  ## Benchmarks
79
69
 
80
- **jiren** delivers native performance by bypassing the JavaScript engine overhead for network I/O.
70
+ **Jiren** delivers native performance by bypassing the JavaScript engine overhead for network I/O.
81
71
 
82
72
  | Client | Requests/sec | Relative Speed |
83
73
  | ----------------- | ------------- | -------------- |
84
- | **jiren** | **1,550,000** | **1.0x** |
74
+ | **Jiren** | **1,550,000** | **1.0x** |
85
75
  | Bun `fetch` | 45,000 | 34x Slower |
86
76
  | Node `http` | 32,000 | 48x Slower |
87
77
  | Python `requests` | 6,500 | 238x Slower |
@@ -1,18 +1,23 @@
1
- import { CString, type Pointer } from "bun:ffi";
1
+ import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
2
2
  import { lib } from "./native";
3
- import type {
4
- RequestOptions,
5
- GetRequestOptions,
6
- PostRequestOptions,
7
- HttpResponse,
8
- } from "../types";
3
+ import type { RequestOptions } from "./types";
4
+
5
+ export interface JirenClientOptions {
6
+ /** URLs to warmup on client creation (pre-connect + handshake) */
7
+ warmup?: string[];
8
+ }
9
9
 
10
10
  export class JirenClient {
11
11
  private ptr: Pointer | null;
12
12
 
13
- constructor() {
13
+ constructor(options?: JirenClientOptions) {
14
14
  this.ptr = lib.symbols.zclient_new();
15
15
  if (!this.ptr) throw new Error("Failed to create native client instance");
16
+
17
+ // Warmup connections immediately if URLs provided
18
+ if (options?.warmup && options.warmup.length > 0) {
19
+ this.warmup(options.warmup);
20
+ }
16
21
  }
17
22
 
18
23
  /**
@@ -27,35 +32,85 @@ export class JirenClient {
27
32
  }
28
33
 
29
34
  /**
30
- * Perform a HTTP request.
35
+ * Warm up connections to URLs (DNS resolve + QUIC handshake).
36
+ * Call this early (e.g., at app startup) so subsequent requests are fast.
37
+ * @param urls - List of URLs to warm up
38
+ */
39
+ public warmup(urls: string[]): void {
40
+ if (!this.ptr) throw new Error("Client is closed");
41
+
42
+ for (const url of urls) {
43
+ const urlBuffer = Buffer.from(url + "\0");
44
+ lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @deprecated Use warmup() instead
50
+ */
51
+ public prefetch(urls: string[]): void {
52
+ this.warmup(urls);
53
+ }
54
+
55
+ /**
56
+ * Perform a generic HTTP request.
57
+ * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
31
58
  * @param url - The URL to request
32
- * @param options - Request options (method, headers, body)
33
- * @returns Response object
59
+ * @param body - The body content string (optional)
60
+ * @param options - Request options (headers, maxRedirects, etc.) or just headers map
61
+ * @returns Promise resolving to Response object
34
62
  */
35
- public request(url: string, options: RequestOptions = {}): HttpResponse {
63
+ public async request<T = any>(
64
+ method: string,
65
+ url: string,
66
+ body?: string | null,
67
+ options?: RequestOptions | Record<string, string> | null
68
+ ) {
36
69
  if (!this.ptr) throw new Error("Client is closed");
37
70
 
38
- const method = options.method || "GET";
71
+ // Normalize options
72
+ let headers: Record<string, string> = {};
73
+ let maxRedirects = 5; // Default
74
+
75
+ if (options) {
76
+ if ("maxRedirects" in options || "headers" in options) {
77
+ // It is RequestOptions
78
+ const opts = options as RequestOptions;
79
+ if (opts.headers) headers = opts.headers;
80
+ if (opts.maxRedirects !== undefined) maxRedirects = opts.maxRedirects;
81
+ // Merge top-level unknown keys as headers if lenient? No, strict to types.
82
+ } else {
83
+ // Assume it's just headers Record<string, string> for backward compatibility
84
+ headers = options as Record<string, string>;
85
+ }
86
+ }
87
+
39
88
  const methodBuffer = Buffer.from(method + "\0");
40
89
  const urlBuffer = Buffer.from(url + "\0");
41
90
 
42
- let headersBuffer: Buffer | null = null;
43
- if (options.headers) {
44
- const headerStr = Object.entries(options.headers)
45
- .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
46
- .join("\r\n");
47
- if (headerStr.length > 0) {
48
- headersBuffer = Buffer.from(headerStr + "\0");
49
- }
91
+ let bodyBuffer: Buffer | null = null;
92
+ if (body) {
93
+ bodyBuffer = Buffer.from(body + "\0");
50
94
  }
51
95
 
52
- let bodyBuffer: Buffer | null = null;
53
- if (options.body) {
54
- const bodyStr =
55
- typeof options.body === "string"
56
- ? options.body
57
- : JSON.stringify(options.body);
58
- bodyBuffer = Buffer.from(bodyStr + "\0");
96
+ let headersBuffer: Buffer | null = null;
97
+ const defaultHeaders: Record<string, string> = {
98
+ "user-agent":
99
+ "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
+ accept:
101
+ "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",
103
+ "accept-language": "en-US,en;q=0.9",
104
+ };
105
+
106
+ const finalHeaders = { ...defaultHeaders, ...headers };
107
+
108
+ const headerStr = Object.entries(finalHeaders)
109
+ .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
110
+ .join("\r\n");
111
+
112
+ if (headerStr.length > 0) {
113
+ headersBuffer = Buffer.from(headerStr + "\0");
59
114
  }
60
115
 
61
116
  const respPtr = lib.symbols.zclient_request(
@@ -63,123 +118,80 @@ export class JirenClient {
63
118
  methodBuffer,
64
119
  urlBuffer,
65
120
  headersBuffer,
66
- bodyBuffer
121
+ bodyBuffer,
122
+ maxRedirects
67
123
  );
68
124
 
69
- const response = this.parseResponse(respPtr);
70
-
71
- // Handle Redirects
72
- if (
73
- options.maxRedirects &&
74
- options.maxRedirects > 0 &&
75
- response.status >= 300 &&
76
- response.status < 400 &&
77
- response.headers &&
78
- response.headers["location"]
79
- ) {
80
- const location = response.headers["location"];
81
- const newUrl = new URL(location, url).toString(); // Resolve relative URLs
82
- const newOptions = { ...options, maxRedirects: options.maxRedirects - 1 };
83
-
84
- return this.request(newUrl, newOptions);
85
- }
86
-
87
- return response;
125
+ return this.parseResponse<T>(respPtr);
88
126
  }
89
127
 
90
- public get(url: string, options: GetRequestOptions = {}) {
91
- return this.request(url, { ...options, method: "GET" });
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);
92
133
  }
93
134
 
94
- public post(
135
+ public async post<T = any>(
95
136
  url: string,
96
- body: string | object,
97
- options: PostRequestOptions = {}
137
+ body: string,
138
+ options?: RequestOptions | Record<string, string>
98
139
  ) {
99
- return this.request(url, { ...options, method: "POST", body });
140
+ return this.request<T>("POST", url, body, options);
100
141
  }
101
142
 
102
- public put(
143
+ public async put<T = any>(
103
144
  url: string,
104
- body: string | object,
105
- options: PostRequestOptions = {}
145
+ body: string,
146
+ options?: RequestOptions | Record<string, string>
106
147
  ) {
107
- return this.request(url, { ...options, method: "PUT", body });
148
+ return this.request<T>("PUT", url, body, options);
108
149
  }
109
150
 
110
- public delete(url: string, options: GetRequestOptions = {}) {
111
- return this.request(url, { ...options, method: "DELETE" });
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);
112
157
  }
113
158
 
114
- public patch(
159
+ public async delete<T = any>(
115
160
  url: string,
116
- body: string | object,
117
- options: PostRequestOptions = {}
161
+ body?: string,
162
+ options?: RequestOptions | Record<string, string>
118
163
  ) {
119
- return this.request(url, { ...options, method: "PATCH", body });
164
+ return this.request<T>("DELETE", url, body || null, options);
120
165
  }
121
166
 
122
- public head(url: string, options: GetRequestOptions = {}) {
123
- return this.request(url, { ...options, method: "HEAD" });
167
+ public async head(url: string, headers?: Record<string, string>) {
168
+ return this.request("HEAD", url, null, headers);
124
169
  }
125
170
 
126
- /**
127
- * Prefetch URLs to warm up connections (resolve DNS & Handshake).
128
- * @param urls - List of URLs to prefetch
129
- */
130
- public prefetch(urls: string[]) {
131
- if (!this.ptr) throw new Error("Client is closed");
132
-
133
- for (const url of urls) {
134
- const urlBuffer = Buffer.from(url + "\0");
135
- lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
136
- }
171
+ public async options(url: string, headers?: Record<string, string>) {
172
+ return this.request("OPTIONS", url, null, headers);
137
173
  }
138
174
 
139
- private parseResponse(respPtr: Pointer | null) {
175
+ private parseResponse<T = any>(respPtr: Pointer | null) {
140
176
  if (!respPtr)
141
177
  throw new Error("Native request failed (returned null pointer)");
142
178
 
143
179
  try {
144
180
  const status = lib.symbols.zclient_response_status(respPtr);
145
- const bodyLen = Number(lib.symbols.zclient_response_body_len(respPtr));
181
+ const len = Number(lib.symbols.zclient_response_body_len(respPtr));
146
182
  const bodyPtr = lib.symbols.zclient_response_body(respPtr);
147
- const headersLen = Number(
148
- lib.symbols.zclient_response_headers_len(respPtr)
149
- );
150
- const headersPtr = lib.symbols.zclient_response_headers(respPtr);
151
183
 
152
184
  let bodyString = "";
153
- if (bodyLen > 0 && bodyPtr) {
154
- // CString uses the full length if not null-terminated or we can just read the ptr
155
- // Since we know the length, we can be more precise, but CString assumes null-terminated
156
- // for now we trust it is null terminated from Zig
157
- bodyString = new CString(bodyPtr).toString();
158
- }
159
-
160
- const headers: Record<string, string> = {};
161
- if (headersLen > 0 && headersPtr) {
162
- const headersStr = new CString(headersPtr).toString();
163
- const lines = headersStr.split("\r\n");
164
- for (const line of lines) {
165
- if (!line) continue;
166
- const colonIdx = line.indexOf(":");
167
- if (colonIdx !== -1) {
168
- const key = line.substring(0, colonIdx).trim().toLowerCase();
169
- const val = line.substring(colonIdx + 1).trim();
170
- headers[key] = val;
171
- }
172
- }
185
+ if (len > 0 && bodyPtr) {
186
+ const buffer = toArrayBuffer(bodyPtr, 0, len);
187
+ bodyString = new TextDecoder().decode(buffer);
173
188
  }
174
189
 
175
190
  return {
176
191
  status,
177
192
  body: bodyString,
178
- headers,
179
- json: <T = any>() => {
180
- if (!bodyString) return null as T;
181
- return JSON.parse(bodyString) as T;
182
- },
193
+ text: async () => bodyString,
194
+ json: async (): Promise<T> => JSON.parse(bodyString),
183
195
  };
184
196
  } finally {
185
197
  lib.symbols.zclient_response_free(respPtr);
@@ -1,11 +1,11 @@
1
1
  /**
2
- * flous - Ultra-fast HTTP/HTTPS client powered by native Zig
2
+ * Jiren - Ultra-fast HTTP/3 client powered by native Zig + QUIC
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
6
 
7
7
  // Main client
8
- export { JirenClient } from "./client";
8
+ export { JirenClient, type JirenClientOptions } from "./client";
9
9
 
10
10
  // Types
11
11
  export type { JirenHttpConfig, ParsedUrl } from "../types/index";
@@ -21,6 +21,17 @@ export const ffiDef = {
21
21
  args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
22
22
  returns: FFIType.ptr,
23
23
  },
24
+ zclient_request: {
25
+ args: [
26
+ FFIType.ptr,
27
+ FFIType.cstring,
28
+ FFIType.cstring,
29
+ FFIType.cstring,
30
+ FFIType.cstring,
31
+ FFIType.u8,
32
+ ],
33
+ returns: FFIType.ptr,
34
+ },
24
35
  zclient_prefetch: {
25
36
  args: [FFIType.ptr, FFIType.cstring],
26
37
  returns: FFIType.void,
@@ -49,16 +60,6 @@ export const ffiDef = {
49
60
  args: [FFIType.ptr],
50
61
  returns: FFIType.void,
51
62
  },
52
- zclient_request: {
53
- args: [
54
- FFIType.ptr, // client
55
- FFIType.cstring, // method
56
- FFIType.cstring, // url
57
- FFIType.cstring, // headers (nullable)
58
- FFIType.cstring, // body (nullable)
59
- ],
60
- returns: FFIType.ptr,
61
- },
62
63
  } as const;
63
64
 
64
65
  export const lib = dlopen(libPath, ffiDef);
@@ -32,6 +32,8 @@ export interface RequestOptions {
32
32
  body?: string | object;
33
33
  /** Request timeout in milliseconds */
34
34
  timeout?: number;
35
+ /** Maximum number of redirects to follow */
36
+ maxRedirects?: number;
35
37
  }
36
38
 
37
39
  /** HTTP Response */
@@ -0,0 +1,137 @@
1
+ import { parentPort } from "worker_threads";
2
+ import { CString, type Pointer } from "bun:ffi";
3
+ import { lib } from "./native";
4
+ import type { RequestOptions } from "../types";
5
+
6
+ // Each worker has its own isolated native client
7
+ let ptr: Pointer | null = lib.symbols.zclient_new();
8
+
9
+ if (!ptr) {
10
+ throw new Error("Failed to create native client instance in worker");
11
+ }
12
+
13
+ // Handle cleanup
14
+ process.on("exit", () => {
15
+ if (ptr) {
16
+ lib.symbols.zclient_free(ptr);
17
+ ptr = null;
18
+ }
19
+ });
20
+
21
+ if (parentPort) {
22
+ parentPort.on(
23
+ "message",
24
+ (msg: {
25
+ id: number;
26
+ url: string;
27
+ options: RequestOptions;
28
+ type?: string;
29
+ }) => {
30
+ if (msg.type === "close") {
31
+ if (ptr) {
32
+ lib.symbols.zclient_free(ptr);
33
+ ptr = null;
34
+ }
35
+ process.exit(0);
36
+ return;
37
+ }
38
+
39
+ if (msg.type === "prefetch") {
40
+ if (!ptr) return;
41
+ const urls = msg.url as unknown as string[]; // hack: url field used for string[]
42
+ for (const url of urls) {
43
+ const urlBuffer = Buffer.from(url + "\0");
44
+ lib.symbols.zclient_prefetch(ptr, urlBuffer);
45
+ }
46
+ parentPort?.postMessage({ type: "prefetch_done" });
47
+ return;
48
+ }
49
+
50
+ const { id, url, options } = msg;
51
+
52
+ try {
53
+ if (!ptr) throw new Error("Worker client is closed");
54
+
55
+ const method = options.method || "GET";
56
+ const methodBuffer = Buffer.from(method + "\0");
57
+ const urlBuffer = Buffer.from(url + "\0");
58
+
59
+ let headersBuffer: Buffer | null = null;
60
+ if (options.headers) {
61
+ const headerStr = Object.entries(options.headers)
62
+ .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
63
+ .join("\r\n");
64
+ if (headerStr.length > 0) {
65
+ headersBuffer = Buffer.from(headerStr + "\0");
66
+ }
67
+ }
68
+
69
+ let bodyBuffer: Buffer | null = null;
70
+ if (options.body) {
71
+ const bodyStr =
72
+ typeof options.body === "string"
73
+ ? options.body
74
+ : JSON.stringify(options.body);
75
+ bodyBuffer = Buffer.from(bodyStr + "\0");
76
+ }
77
+
78
+ const respPtr = lib.symbols.zclient_request(
79
+ ptr,
80
+ methodBuffer,
81
+ urlBuffer,
82
+ headersBuffer,
83
+ bodyBuffer
84
+ );
85
+
86
+ // Parse Response
87
+ if (!respPtr) throw new Error("Native request failed");
88
+
89
+ const status = lib.symbols.zclient_response_status(respPtr);
90
+ const bodyLen = Number(lib.symbols.zclient_response_body_len(respPtr));
91
+ const bodyPtr = lib.symbols.zclient_response_body(respPtr);
92
+ const headersLen = Number(
93
+ lib.symbols.zclient_response_headers_len(respPtr)
94
+ );
95
+ const headersPtr = lib.symbols.zclient_response_headers(respPtr);
96
+
97
+ let bodyString = "";
98
+ if (bodyLen > 0 && bodyPtr) {
99
+ bodyString = new CString(bodyPtr).toString();
100
+ }
101
+
102
+ const headers: Record<string, string> = {};
103
+ if (headersLen > 0 && headersPtr) {
104
+ const headersStr = new CString(headersPtr).toString();
105
+ const lines = headersStr.split("\r\n");
106
+ for (const line of lines) {
107
+ if (!line) continue;
108
+ const colonIdx = line.indexOf(":");
109
+ if (colonIdx !== -1) {
110
+ const key = line.substring(0, colonIdx).trim().toLowerCase();
111
+ const val = line.substring(colonIdx + 1).trim();
112
+ headers[key] = val;
113
+ }
114
+ }
115
+ }
116
+
117
+ lib.symbols.zclient_response_free(respPtr);
118
+
119
+ parentPort?.postMessage({
120
+ id,
121
+ success: true,
122
+ response: {
123
+ status,
124
+ body: bodyString,
125
+ headers,
126
+ },
127
+ });
128
+ } catch (err: any) {
129
+ parentPort?.postMessage({
130
+ id,
131
+ success: false,
132
+ error: err.message,
133
+ });
134
+ }
135
+ }
136
+ );
137
+ }
package/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * jiren - Ultra-fast HTTP/HTTPS client powered by native Zig
3
+ *
4
+ * Faster than any other HTTP/HTTPS client | 1.5M+ requests/sec
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { JirenClient } from 'jiren';
9
+ *
10
+ * const client = new JirenClient();
11
+ *
12
+ * // High-performance batch requests
13
+ * const result = client.batch('http://localhost:3000/', {
14
+ * count: 10000,
15
+ * threads: 100
16
+ * });
17
+ *
18
+ * console.log(`Success: ${result.success}/${result.total}`);
19
+ * console.log(`Speed: ${result.requestsPerSecond.toFixed(0)} req/sec`);
20
+ * ```
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+
25
+ // Main client
26
+ export { JirenClient } from "./components";
27
+
28
+ // Types
29
+ export * from "./types";
Binary file
package/package.json CHANGED
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.0.15",
3
+ "version": "1.1.1",
4
4
  "author": "",
5
- "main": "dist/index.js",
6
- "module": "dist/index.js",
7
- "types": "types/index.ts",
5
+ "main": "index.ts",
6
+ "module": "index.ts",
7
+ "types": "index.ts",
8
8
  "devDependencies": {
9
- "@types/bun": "^1.3.4",
10
- "bun-types": "^1.3.4"
9
+ "@types/bun": "^1.3.4"
11
10
  },
12
11
  "peerDependencies": {
13
12
  "typescript": "^5"
14
13
  },
15
14
  "description": "Jiren is a high-performance HTTP/HTTPS client, Faster than any other HTTP/HTTPS client.",
16
15
  "files": [
17
- "dist",
16
+ "index.ts",
17
+ "types",
18
18
  "lib",
19
- "components",
20
- "types"
19
+ "components"
21
20
  ],
22
21
  "keywords": [
23
22
  "http",
@@ -34,11 +33,10 @@
34
33
  "license": "MIT",
35
34
  "scripts": {
36
35
  "build:zig": "cd .. && zig build --release=fast",
37
- "build": "bun build ./index.ts --outdir dist --target bun",
38
- "prepare": "bun run build",
39
36
  "test": "bun run examples/basic.ts"
40
37
  },
41
38
  "engines": {
39
+ "node": ">=18.0.0",
42
40
  "bun": ">=1.1.0"
43
41
  },
44
42
  "type": "module"
package/types/index.ts CHANGED
@@ -23,27 +23,18 @@ export interface BatchResult {
23
23
  requestsPerSecond: number;
24
24
  }
25
25
 
26
- /** Options for single HTTP requests */
27
26
  /** Options for single HTTP requests */
28
27
  export interface RequestOptions {
29
28
  /** HTTP method (default: GET) */
30
- method?: string;
29
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
31
30
  /** Request headers */
32
31
  headers?: Record<string, string>;
33
32
  /** Request body (for POST, PUT, PATCH) */
34
33
  body?: string | object;
35
34
  /** Request timeout in milliseconds */
36
35
  timeout?: number;
37
- /** Maximum number of redirects to follow (default: 0) */
38
- maxRedirects?: number;
39
36
  }
40
37
 
41
- /** Options for requests without a body (GET, DELETE, HEAD) */
42
- export type GetRequestOptions = Omit<RequestOptions, "method" | "body">;
43
-
44
- /** Options for requests with a body (POST, PUT, PATCH) where body is a separate argument */
45
- export type PostRequestOptions = Omit<RequestOptions, "method" | "body">;
46
-
47
38
  /** HTTP Response */
48
39
  export interface HttpResponse {
49
40
  /** HTTP status code */
@@ -52,8 +43,6 @@ export interface HttpResponse {
52
43
  body: string;
53
44
  /** Response headers */
54
45
  headers?: Record<string, string>;
55
- /** Helper to parse JSON body */
56
- json: <T = any>() => T;
57
46
  }
58
47
 
59
48
  /** Configuration for JirenHttpClient */
package/dist/index.js DELETED
@@ -1,182 +0,0 @@
1
- // @bun
2
- // components/client.ts
3
- import { CString } from "bun:ffi";
4
-
5
- // components/native.ts
6
- import { dlopen, FFIType, suffix } from "bun:ffi";
7
- import { join } from "path";
8
- var libPath = join(import.meta.dir, `../lib/libhttpclient.${suffix}`);
9
- var ffiDef = {
10
- zclient_new: {
11
- args: [],
12
- returns: FFIType.ptr
13
- },
14
- zclient_free: {
15
- args: [FFIType.ptr],
16
- returns: FFIType.void
17
- },
18
- zclient_get: {
19
- args: [FFIType.ptr, FFIType.cstring],
20
- returns: FFIType.ptr
21
- },
22
- zclient_post: {
23
- args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
24
- returns: FFIType.ptr
25
- },
26
- zclient_prefetch: {
27
- args: [FFIType.ptr, FFIType.cstring],
28
- returns: FFIType.void
29
- },
30
- zclient_response_status: {
31
- args: [FFIType.ptr],
32
- returns: FFIType.u16
33
- },
34
- zclient_response_body: {
35
- args: [FFIType.ptr],
36
- returns: FFIType.ptr
37
- },
38
- zclient_response_body_len: {
39
- args: [FFIType.ptr],
40
- returns: FFIType.u64
41
- },
42
- zclient_response_headers: {
43
- args: [FFIType.ptr],
44
- returns: FFIType.ptr
45
- },
46
- zclient_response_headers_len: {
47
- args: [FFIType.ptr],
48
- returns: FFIType.u64
49
- },
50
- zclient_response_free: {
51
- args: [FFIType.ptr],
52
- returns: FFIType.void
53
- },
54
- zclient_request: {
55
- args: [
56
- FFIType.ptr,
57
- FFIType.cstring,
58
- FFIType.cstring,
59
- FFIType.cstring,
60
- FFIType.cstring
61
- ],
62
- returns: FFIType.ptr
63
- }
64
- };
65
- var lib = dlopen(libPath, ffiDef);
66
-
67
- // components/client.ts
68
- class JirenClient {
69
- ptr;
70
- constructor() {
71
- this.ptr = lib.symbols.zclient_new();
72
- if (!this.ptr)
73
- throw new Error("Failed to create native client instance");
74
- }
75
- close() {
76
- if (this.ptr) {
77
- lib.symbols.zclient_free(this.ptr);
78
- this.ptr = null;
79
- }
80
- }
81
- request(url, options = {}) {
82
- if (!this.ptr)
83
- throw new Error("Client is closed");
84
- const method = options.method || "GET";
85
- const methodBuffer = Buffer.from(method + "\x00");
86
- const urlBuffer = Buffer.from(url + "\x00");
87
- let headersBuffer = null;
88
- if (options.headers) {
89
- const headerStr = Object.entries(options.headers).map(([k, v]) => `${k.toLowerCase()}: ${v}`).join(`\r
90
- `);
91
- if (headerStr.length > 0) {
92
- headersBuffer = Buffer.from(headerStr + "\x00");
93
- }
94
- }
95
- let bodyBuffer = null;
96
- if (options.body) {
97
- const bodyStr = typeof options.body === "string" ? options.body : JSON.stringify(options.body);
98
- bodyBuffer = Buffer.from(bodyStr + "\x00");
99
- }
100
- const respPtr = lib.symbols.zclient_request(this.ptr, methodBuffer, urlBuffer, headersBuffer, bodyBuffer);
101
- const response = this.parseResponse(respPtr);
102
- if (options.maxRedirects && options.maxRedirects > 0 && response.status >= 300 && response.status < 400 && response.headers && response.headers["location"]) {
103
- const location = response.headers["location"];
104
- const newUrl = new URL(location, url).toString();
105
- const newOptions = { ...options, maxRedirects: options.maxRedirects - 1 };
106
- return this.request(newUrl, newOptions);
107
- }
108
- return response;
109
- }
110
- get(url, options = {}) {
111
- return this.request(url, { ...options, method: "GET" });
112
- }
113
- post(url, body, options = {}) {
114
- return this.request(url, { ...options, method: "POST", body });
115
- }
116
- put(url, body, options = {}) {
117
- return this.request(url, { ...options, method: "PUT", body });
118
- }
119
- delete(url, options = {}) {
120
- return this.request(url, { ...options, method: "DELETE" });
121
- }
122
- patch(url, body, options = {}) {
123
- return this.request(url, { ...options, method: "PATCH", body });
124
- }
125
- head(url, options = {}) {
126
- return this.request(url, { ...options, method: "HEAD" });
127
- }
128
- prefetch(urls) {
129
- if (!this.ptr)
130
- throw new Error("Client is closed");
131
- for (const url of urls) {
132
- const urlBuffer = Buffer.from(url + "\x00");
133
- lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
134
- }
135
- }
136
- parseResponse(respPtr) {
137
- if (!respPtr)
138
- throw new Error("Native request failed (returned null pointer)");
139
- try {
140
- const status = lib.symbols.zclient_response_status(respPtr);
141
- const bodyLen = Number(lib.symbols.zclient_response_body_len(respPtr));
142
- const bodyPtr = lib.symbols.zclient_response_body(respPtr);
143
- const headersLen = Number(lib.symbols.zclient_response_headers_len(respPtr));
144
- const headersPtr = lib.symbols.zclient_response_headers(respPtr);
145
- let bodyString = "";
146
- if (bodyLen > 0 && bodyPtr) {
147
- bodyString = new CString(bodyPtr).toString();
148
- }
149
- const headers = {};
150
- if (headersLen > 0 && headersPtr) {
151
- const headersStr = new CString(headersPtr).toString();
152
- const lines = headersStr.split(`\r
153
- `);
154
- for (const line of lines) {
155
- if (!line)
156
- continue;
157
- const colonIdx = line.indexOf(":");
158
- if (colonIdx !== -1) {
159
- const key = line.substring(0, colonIdx).trim().toLowerCase();
160
- const val = line.substring(colonIdx + 1).trim();
161
- headers[key] = val;
162
- }
163
- }
164
- }
165
- return {
166
- status,
167
- body: bodyString,
168
- headers,
169
- json: () => {
170
- if (!bodyString)
171
- return null;
172
- return JSON.parse(bodyString);
173
- }
174
- };
175
- } finally {
176
- lib.symbols.zclient_response_free(respPtr);
177
- }
178
- }
179
- }
180
- export {
181
- JirenClient
182
- };