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 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
- - **Zero-Copy (Planned)**: Minimal overhead FFI.
9
- - **HTTP/1.1 & HTTP/3 (QUIC)**: Support for modern protocols.
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 GET
24
+ ### Basic Requests
23
25
 
24
26
  ```typescript
25
- import { JirenClient } from "jiren"; // Class name is JirenClient
27
+ import { JirenClient } from "jiren";
26
28
 
27
29
  const client = new JirenClient();
28
- const res = client.get("https://example.com");
29
- console.log(res.status, res.body.length);
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
@@ -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 GET request.
27
- * @param url - The URL to request (http:// or https://)
28
- * @returns Response object containing status and body string
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 get(url: string) {
35
+ public request(url: string, options: RequestOptions = {}): HttpResponse {
31
36
  if (!this.ptr) throw new Error("Client is closed");
32
37
 
33
- // Buffer.from(string + "\0") is the safe way to create a CString buffer in Bun FFI
34
- // We can also use `Buffer.from(url + "\0")
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
- const respPtr = lib.symbols.zclient_get(this.ptr, urlBuffer);
39
- return this.parseResponse(respPtr);
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
- * Perform a POST request.
44
- * @param url - The URL to request
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
- const urlBuffer = Buffer.from(url + "\0");
52
- const bodyBuffer = Buffer.from(body + "\0");
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
- const respPtr = lib.symbols.zclient_post(this.ptr, urlBuffer, bodyBuffer);
55
- return this.parseResponse(respPtr);
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 len = Number(lib.symbols.zclient_response_body_len(respPtr));
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 (len > 0 && bodyPtr) {
83
- // CString wrapper works with Pointer objects
84
- const cstr = new CString(bodyPtr);
85
- bodyString = cstr.toString();
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 { status, body: bodyString };
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
  }
@@ -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);
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "author": "",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",
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?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
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 */