jiren 1.0.1 → 1.0.3
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 +39 -7
- package/components/client.ts +125 -30
- package/components/native.ts +18 -0
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +1 -1
- package/types/index.ts +12 -1
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@ Designed to be significantly faster than `fetch` and other Node/Bun HTTP clients
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
9
|
-
- **HTTP/
|
|
8
|
+
- **Blazing Fast**: Native implementation with minimal overhead.
|
|
9
|
+
- **HTTP/3 (QUIC) Support**: First-class support for modern, high-performance connections.
|
|
10
|
+
- **Automatic Redirects**: Recursively follows redirects (301, 302, etc.) with cycle protection.
|
|
11
|
+
- **Type-Safe API**: Strict options (`GetRequestOptions`, `PostRequestOptions`) for better DX.
|
|
12
|
+
- **Helper Methods**: Built-in `.json<T>()` parser and header accessors.
|
|
10
13
|
- **Connection Pooling**: Reuse connections for maximum throughput.
|
|
11
14
|
- **0-RTT Session Resumption**: Extremely fast subsequent requests.
|
|
12
|
-
- **Prefetching**: Warm up connections in advance.
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
15
17
|
|
|
@@ -19,14 +21,44 @@ bun add jiren
|
|
|
19
21
|
|
|
20
22
|
## Usage
|
|
21
23
|
|
|
22
|
-
### Basic
|
|
24
|
+
### Basic Requests
|
|
23
25
|
|
|
24
26
|
```typescript
|
|
25
|
-
import { JirenClient } from "jiren";
|
|
27
|
+
import { JirenClient } from "jiren";
|
|
26
28
|
|
|
27
29
|
const client = new JirenClient();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
// Simple GET
|
|
32
|
+
const res = client.get("https://google.com");
|
|
33
|
+
console.log(res.status); // 200
|
|
34
|
+
|
|
35
|
+
// JSON Parsing Helper
|
|
36
|
+
const data = res.json<{ message: string }>();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Advanced Options (Headers, Redirects, Body)
|
|
40
|
+
|
|
41
|
+
Jiren enforces type safety. You cannot pass a body to a GET request!
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// GET with Headers
|
|
45
|
+
client.get("https://api.example.com", {
|
|
46
|
+
headers: { Authorization: "Bearer token" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// POST with JSON body
|
|
50
|
+
client.post(
|
|
51
|
+
"https://api.example.com/users",
|
|
52
|
+
JSON.stringify({ name: "Jiren" }),
|
|
53
|
+
{
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Follow Redirects (e.g., http -> https)
|
|
59
|
+
client.get("http://google.com", {
|
|
60
|
+
maxRedirects: 5, // Automatically follows up to 5 hops
|
|
61
|
+
});
|
|
30
62
|
```
|
|
31
63
|
|
|
32
64
|
### Prefetching
|
package/components/client.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { CString, 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
9
|
|
|
4
10
|
export class JirenClient {
|
|
5
|
-
// Using Pointer | number | null because Bun returns null or pointer-like object or number
|
|
6
|
-
// dlopen return types with FFIType.ptr are usually mapped to Pointer | null
|
|
7
11
|
private ptr: Pointer | null;
|
|
8
12
|
|
|
9
13
|
constructor() {
|
|
@@ -23,36 +27,100 @@ export class JirenClient {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
* Perform a
|
|
27
|
-
* @param url - The URL to request
|
|
28
|
-
* @
|
|
30
|
+
* Perform a HTTP request.
|
|
31
|
+
* @param url - The URL to request
|
|
32
|
+
* @param options - Request options (method, headers, body)
|
|
33
|
+
* @returns Response object
|
|
29
34
|
*/
|
|
30
|
-
public
|
|
35
|
+
public request(url: string, options: RequestOptions = {}): HttpResponse {
|
|
31
36
|
if (!this.ptr) throw new Error("Client is closed");
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Note: We keep the buffer alive for the duration of the call implicitly
|
|
38
|
+
const method = options.method || "GET";
|
|
39
|
+
const methodBuffer = Buffer.from(method + "\0");
|
|
36
40
|
const urlBuffer = Buffer.from(url + "\0");
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
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");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const respPtr = lib.symbols.zclient_request(
|
|
62
|
+
this.ptr,
|
|
63
|
+
methodBuffer,
|
|
64
|
+
urlBuffer,
|
|
65
|
+
headersBuffer,
|
|
66
|
+
bodyBuffer
|
|
67
|
+
);
|
|
68
|
+
|
|
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;
|
|
40
88
|
}
|
|
41
89
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* @param body - The body content string
|
|
46
|
-
* @returns Response object
|
|
47
|
-
*/
|
|
48
|
-
public post(url: string, body: string) {
|
|
49
|
-
if (!this.ptr) throw new Error("Client is closed");
|
|
90
|
+
public get(url: string, options: GetRequestOptions = {}) {
|
|
91
|
+
return this.request(url, { ...options, method: "GET" });
|
|
92
|
+
}
|
|
50
93
|
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
public post(
|
|
95
|
+
url: string,
|
|
96
|
+
body: string | object,
|
|
97
|
+
options: PostRequestOptions = {}
|
|
98
|
+
) {
|
|
99
|
+
return this.request(url, { ...options, method: "POST", body });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public put(
|
|
103
|
+
url: string,
|
|
104
|
+
body: string | object,
|
|
105
|
+
options: PostRequestOptions = {}
|
|
106
|
+
) {
|
|
107
|
+
return this.request(url, { ...options, method: "PUT", body });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public delete(url: string, options: GetRequestOptions = {}) {
|
|
111
|
+
return this.request(url, { ...options, method: "DELETE" });
|
|
112
|
+
}
|
|
53
113
|
|
|
54
|
-
|
|
55
|
-
|
|
114
|
+
public patch(
|
|
115
|
+
url: string,
|
|
116
|
+
body: string | object,
|
|
117
|
+
options: PostRequestOptions = {}
|
|
118
|
+
) {
|
|
119
|
+
return this.request(url, { ...options, method: "PATCH", body });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public head(url: string, options: GetRequestOptions = {}) {
|
|
123
|
+
return this.request(url, { ...options, method: "HEAD" });
|
|
56
124
|
}
|
|
57
125
|
|
|
58
126
|
/**
|
|
@@ -73,19 +141,46 @@ export class JirenClient {
|
|
|
73
141
|
throw new Error("Native request failed (returned null pointer)");
|
|
74
142
|
|
|
75
143
|
try {
|
|
76
|
-
// In FFI, we can pass the pointer directly to these getters
|
|
77
144
|
const status = lib.symbols.zclient_response_status(respPtr);
|
|
78
|
-
const
|
|
145
|
+
const bodyLen = Number(lib.symbols.zclient_response_body_len(respPtr));
|
|
79
146
|
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);
|
|
80
151
|
|
|
81
152
|
let bodyString = "";
|
|
82
|
-
if (
|
|
83
|
-
// CString
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
86
173
|
}
|
|
87
174
|
|
|
88
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
status,
|
|
177
|
+
body: bodyString,
|
|
178
|
+
headers,
|
|
179
|
+
json: <T = any>() => {
|
|
180
|
+
if (!bodyString) return null as T;
|
|
181
|
+
return JSON.parse(bodyString) as T;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
89
184
|
} finally {
|
|
90
185
|
lib.symbols.zclient_response_free(respPtr);
|
|
91
186
|
}
|
package/components/native.ts
CHANGED
|
@@ -37,10 +37,28 @@ export const ffiDef = {
|
|
|
37
37
|
args: [FFIType.ptr],
|
|
38
38
|
returns: FFIType.u64,
|
|
39
39
|
},
|
|
40
|
+
zclient_response_headers: {
|
|
41
|
+
args: [FFIType.ptr],
|
|
42
|
+
returns: FFIType.ptr,
|
|
43
|
+
},
|
|
44
|
+
zclient_response_headers_len: {
|
|
45
|
+
args: [FFIType.ptr],
|
|
46
|
+
returns: FFIType.u64,
|
|
47
|
+
},
|
|
40
48
|
zclient_response_free: {
|
|
41
49
|
args: [FFIType.ptr],
|
|
42
50
|
returns: FFIType.void,
|
|
43
51
|
},
|
|
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
|
+
},
|
|
44
62
|
} as const;
|
|
45
63
|
|
|
46
64
|
export const lib = dlopen(libPath, ffiDef);
|
package/lib/libhttpclient.dylib
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/types/index.ts
CHANGED
|
@@ -23,18 +23,27 @@ export interface BatchResult {
|
|
|
23
23
|
requestsPerSecond: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Options for single HTTP requests */
|
|
26
27
|
/** Options for single HTTP requests */
|
|
27
28
|
export interface RequestOptions {
|
|
28
29
|
/** HTTP method (default: GET) */
|
|
29
|
-
method?:
|
|
30
|
+
method?: string;
|
|
30
31
|
/** Request headers */
|
|
31
32
|
headers?: Record<string, string>;
|
|
32
33
|
/** Request body (for POST, PUT, PATCH) */
|
|
33
34
|
body?: string | object;
|
|
34
35
|
/** Request timeout in milliseconds */
|
|
35
36
|
timeout?: number;
|
|
37
|
+
/** Maximum number of redirects to follow (default: 0) */
|
|
38
|
+
maxRedirects?: number;
|
|
36
39
|
}
|
|
37
40
|
|
|
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
|
+
|
|
38
47
|
/** HTTP Response */
|
|
39
48
|
export interface HttpResponse {
|
|
40
49
|
/** HTTP status code */
|
|
@@ -43,6 +52,8 @@ export interface HttpResponse {
|
|
|
43
52
|
body: string;
|
|
44
53
|
/** Response headers */
|
|
45
54
|
headers?: Record<string, string>;
|
|
55
|
+
/** Helper to parse JSON body */
|
|
56
|
+
json: <T = any>() => T;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
/** Configuration for JirenHttpClient */
|