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 +33 -43
- package/components/client.ts +120 -108
- package/components/index.ts +2 -2
- package/components/native.ts +11 -10
- package/components/types.ts +2 -0
- package/components/worker.ts +137 -0
- package/index.ts +29 -0
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +9 -11
- package/types/index.ts +1 -12
- package/dist/index.js +0 -182
package/README.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Jiren
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
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
|
|
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
|
-
|
|
34
|
-
const
|
|
35
|
-
console.log(
|
|
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
|
-
###
|
|
42
|
-
|
|
43
|
-
Jiren enforces type safety. You cannot pass a body to a GET request!
|
|
37
|
+
### JSON Response
|
|
44
38
|
|
|
45
39
|
```typescript
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
72
|
-
client
|
|
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
|
-
**
|
|
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
|
-
| **
|
|
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 |
|
package/components/client.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
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
|
-
*
|
|
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
|
|
33
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
|
43
|
-
if (
|
|
44
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
|
|
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
|
|
97
|
-
options
|
|
137
|
+
body: string,
|
|
138
|
+
options?: RequestOptions | Record<string, string>
|
|
98
139
|
) {
|
|
99
|
-
return this.request(
|
|
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
|
|
105
|
-
options
|
|
145
|
+
body: string,
|
|
146
|
+
options?: RequestOptions | Record<string, string>
|
|
106
147
|
) {
|
|
107
|
-
return this.request(
|
|
148
|
+
return this.request<T>("PUT", url, body, options);
|
|
108
149
|
}
|
|
109
150
|
|
|
110
|
-
public
|
|
111
|
-
|
|
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
|
|
159
|
+
public async delete<T = any>(
|
|
115
160
|
url: string,
|
|
116
|
-
body
|
|
117
|
-
options
|
|
161
|
+
body?: string,
|
|
162
|
+
options?: RequestOptions | Record<string, string>
|
|
118
163
|
) {
|
|
119
|
-
return this.request(url,
|
|
164
|
+
return this.request<T>("DELETE", url, body || null, options);
|
|
120
165
|
}
|
|
121
166
|
|
|
122
|
-
public head(url: string,
|
|
123
|
-
return this.request(url,
|
|
167
|
+
public async head(url: string, headers?: Record<string, string>) {
|
|
168
|
+
return this.request("HEAD", url, null, headers);
|
|
124
169
|
}
|
|
125
170
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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 (
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
179
|
-
json: <T
|
|
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);
|
package/components/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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";
|
package/components/native.ts
CHANGED
|
@@ -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);
|
package/components/types.ts
CHANGED
|
@@ -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";
|
package/lib/libhttpclient.dylib
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jiren",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"author": "",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
7
|
-
"types": "
|
|
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
|
-
"
|
|
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?:
|
|
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
|
-
};
|