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 +193 -30
- package/components/client.ts +508 -69
- package/components/index.ts +12 -2
- package/components/native.ts +21 -0
- package/components/types.ts +116 -4
- package/components/worker.ts +6 -1
- package/lib/libcurl-impersonate.dylib +0 -0
- package/lib/libhttpclient.dylib +0 -0
- package/lib/libidn2.0.dylib +0 -0
- package/lib/libintl.8.dylib +0 -0
- package/lib/libunistring.5.dylib +0 -0
- package/lib/libzstd.1.5.7.dylib +0 -0
- package/package.json +1 -1
- package/types/index.ts +6 -0
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
|
-
- **
|
|
10
|
-
- **HTTP/1.1 & HTTP/3 (QUIC)**:
|
|
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
|
-
- **
|
|
13
|
-
- **
|
|
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
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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:
|
|
88
|
+
warmup: {
|
|
89
|
+
api: "https://api.myservice.com",
|
|
90
|
+
},
|
|
59
91
|
});
|
|
60
92
|
|
|
61
|
-
//
|
|
62
|
-
client.
|
|
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
|
-
//
|
|
65
|
-
const
|
|
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
|
-
##
|
|
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
|
-
**
|
|
238
|
+
JirenClient is often **faster than Bun's native fetch** thanks to optimized connection pooling and native Zig implementation.
|
|
71
239
|
|
|
72
|
-
|
|
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
|
-
|
|
242
|
+
MIT
|
package/components/client.ts
CHANGED
|
@@ -1,23 +1,258 @@
|
|
|
1
1
|
import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
2
2
|
import { lib } from "./native";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RequestOptions,
|
|
5
|
+
JirenResponse,
|
|
6
|
+
JirenResponseBody,
|
|
7
|
+
WarmupUrlConfig,
|
|
8
|
+
UrlRequestOptions,
|
|
9
|
+
UrlEndpoint,
|
|
10
|
+
} from "./types";
|
|
4
11
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
18
|
-
if (options?.
|
|
19
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
return this.request("OPTIONS", url, null, headers);
|
|
450
|
+
return response;
|
|
173
451
|
}
|
|
174
452
|
|
|
175
|
-
private parseResponse<T = any>(
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
package/components/index.ts
CHANGED
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main client
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
JirenClient,
|
|
10
|
+
type JirenClientOptions,
|
|
11
|
+
type UrlAccessor,
|
|
12
|
+
} from "./client";
|
|
9
13
|
|
|
10
14
|
// Types
|
|
11
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
JirenHttpConfig,
|
|
17
|
+
ParsedUrl,
|
|
18
|
+
WarmupUrlConfig,
|
|
19
|
+
UrlRequestOptions,
|
|
20
|
+
UrlEndpoint,
|
|
21
|
+
} from "./types";
|
|
12
22
|
|
|
13
23
|
// Remove broken exports
|
package/components/native.ts
CHANGED
|
@@ -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);
|
package/components/types.ts
CHANGED
|
@@ -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
|
|
58
|
+
export interface JirenResponse<T = any> {
|
|
41
59
|
/** HTTP status code */
|
|
42
60
|
status: number;
|
|
43
|
-
/** Response body
|
|
44
|
-
body:
|
|
61
|
+
/** Response body object */
|
|
62
|
+
body: JirenResponseBody<T>;
|
|
45
63
|
/** Response headers */
|
|
46
|
-
headers
|
|
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"];
|
package/components/worker.ts
CHANGED
|
@@ -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
|
package/lib/libhttpclient.dylib
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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 */
|