jiren 1.1.0 → 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 +197 -44
- package/components/client.ts +639 -0
- package/components/index.ts +20 -10
- package/components/native.ts +31 -9
- package/components/types.ts +118 -4
- package/components/worker.ts +142 -0
- package/index.ts +29 -0
- 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 +9 -14
- package/types/index.ts +6 -11
- package/components/client-bun.ts +0 -188
- package/components/client-node.ts +0 -105
- package/components/runtime.ts +0 -11
- package/dist/index.js +0 -271
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
2
|
+
import { lib } from "./native";
|
|
3
|
+
import type {
|
|
4
|
+
RequestOptions,
|
|
5
|
+
JirenResponse,
|
|
6
|
+
JirenResponseBody,
|
|
7
|
+
WarmupUrlConfig,
|
|
8
|
+
UrlRequestOptions,
|
|
9
|
+
UrlEndpoint,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
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
|
+
> {
|
|
33
|
+
/** URLs to warmup on client creation (pre-connect + handshake) */
|
|
34
|
+
warmup?: string[] | T;
|
|
35
|
+
|
|
36
|
+
/** Enable benchmark mode (Force HTTP/2, disable probing) */
|
|
37
|
+
benchmark?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
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
|
+
> {
|
|
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>;
|
|
91
|
+
|
|
92
|
+
constructor(options?: JirenClientOptions<T>) {
|
|
93
|
+
this.ptr = lib.symbols.zclient_new();
|
|
94
|
+
if (!this.ptr) throw new Error("Failed to create native client instance");
|
|
95
|
+
|
|
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
|
+
}
|
|
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
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Free the native client resources.
|
|
260
|
+
* Must be called when the client is no longer needed.
|
|
261
|
+
*/
|
|
262
|
+
public close(): void {
|
|
263
|
+
if (this.ptr) {
|
|
264
|
+
lib.symbols.zclient_free(this.ptr);
|
|
265
|
+
this.ptr = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Warm up connections to URLs (DNS resolve + QUIC handshake).
|
|
271
|
+
* Call this early (e.g., at app startup) so subsequent requests are fast.
|
|
272
|
+
* @param urls - List of URLs to warm up
|
|
273
|
+
*/
|
|
274
|
+
public warmup(urls: string[]): void {
|
|
275
|
+
if (!this.ptr) throw new Error("Client is closed");
|
|
276
|
+
|
|
277
|
+
for (const url of urls) {
|
|
278
|
+
const urlBuffer = Buffer.from(url + "\0");
|
|
279
|
+
lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @deprecated Use warmup() instead
|
|
285
|
+
*/
|
|
286
|
+
public prefetch(urls: string[]): void {
|
|
287
|
+
this.warmup(urls);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Perform a generic HTTP request.
|
|
292
|
+
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
293
|
+
* @param url - The URL to request
|
|
294
|
+
* @param body - The body content string (optional)
|
|
295
|
+
* @param options - Request options (headers, maxRedirects, etc.) or just headers map
|
|
296
|
+
* @returns Promise resolving to Response object or parsed body
|
|
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>>;
|
|
328
|
+
public async request<T = any>(
|
|
329
|
+
method: string,
|
|
330
|
+
url: string,
|
|
331
|
+
body?: string | null,
|
|
332
|
+
options?: RequestOptions | Record<string, string> | null
|
|
333
|
+
): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
|
|
334
|
+
if (!this.ptr) throw new Error("Client is closed");
|
|
335
|
+
|
|
336
|
+
// Normalize options
|
|
337
|
+
let headers: Record<string, string> = {};
|
|
338
|
+
let maxRedirects = 5; // Default
|
|
339
|
+
let responseType: RequestOptions["responseType"] | undefined;
|
|
340
|
+
let antibot = false; // Default
|
|
341
|
+
|
|
342
|
+
if (options) {
|
|
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
|
+
) {
|
|
351
|
+
// It is RequestOptions
|
|
352
|
+
const opts = options as RequestOptions;
|
|
353
|
+
if (opts.headers) headers = opts.headers;
|
|
354
|
+
if (opts.maxRedirects !== undefined) maxRedirects = opts.maxRedirects;
|
|
355
|
+
if (opts.responseType) responseType = opts.responseType;
|
|
356
|
+
if (opts.antibot !== undefined) antibot = opts.antibot;
|
|
357
|
+
} else {
|
|
358
|
+
// Assume it's just headers Record<string, string> for backward compatibility
|
|
359
|
+
headers = options as Record<string, string>;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const methodBuffer = Buffer.from(method + "\0");
|
|
364
|
+
const urlBuffer = Buffer.from(url + "\0");
|
|
365
|
+
|
|
366
|
+
let bodyBuffer: Buffer | null = null;
|
|
367
|
+
if (body) {
|
|
368
|
+
bodyBuffer = Buffer.from(body + "\0");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let headersBuffer: Buffer | null = null;
|
|
372
|
+
const defaultHeaders: Record<string, string> = {
|
|
373
|
+
"user-agent":
|
|
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",
|
|
375
|
+
accept:
|
|
376
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
377
|
+
"accept-encoding": "gzip",
|
|
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",
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const finalHeaders = { ...defaultHeaders, ...headers };
|
|
391
|
+
|
|
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)
|
|
423
|
+
.map(([k, v]) => `${k.toLowerCase()}: ${v}`)
|
|
424
|
+
.join("\r\n");
|
|
425
|
+
|
|
426
|
+
if (headerStr.length > 0) {
|
|
427
|
+
headersBuffer = Buffer.from(headerStr + "\0");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const respPtr = lib.symbols.zclient_request(
|
|
431
|
+
this.ptr,
|
|
432
|
+
methodBuffer,
|
|
433
|
+
urlBuffer,
|
|
434
|
+
headersBuffer,
|
|
435
|
+
bodyBuffer,
|
|
436
|
+
maxRedirects,
|
|
437
|
+
antibot
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const response = this.parseResponse<T>(respPtr, url);
|
|
441
|
+
|
|
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
|
+
}
|
|
449
|
+
|
|
450
|
+
return response;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private parseResponse<T = any>(
|
|
454
|
+
respPtr: Pointer | null,
|
|
455
|
+
url: string
|
|
456
|
+
): JirenResponse<T> {
|
|
457
|
+
if (!respPtr)
|
|
458
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const status = lib.symbols.zclient_response_status(respPtr);
|
|
462
|
+
const len = Number(lib.symbols.zclient_response_body_len(respPtr));
|
|
463
|
+
const bodyPtr = lib.symbols.zclient_response_body(respPtr);
|
|
464
|
+
|
|
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);
|
|
500
|
+
if (len > 0 && bodyPtr) {
|
|
501
|
+
// Create a copy of the buffer because the native response is freed immediately after
|
|
502
|
+
buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
|
|
503
|
+
}
|
|
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
|
+
|
|
545
|
+
return {
|
|
546
|
+
url,
|
|
547
|
+
status,
|
|
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>;
|
|
555
|
+
} finally {
|
|
556
|
+
lib.symbols.zclient_response_free(respPtr);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
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
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Jiren - Ultra-fast HTTP/3 client powered by native Zig + QUIC
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
2
6
|
|
|
3
|
-
//
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
// Main client
|
|
8
|
+
export {
|
|
9
|
+
JirenClient,
|
|
10
|
+
type JirenClientOptions,
|
|
11
|
+
type UrlAccessor,
|
|
12
|
+
} from "./client";
|
|
7
13
|
|
|
14
|
+
// Types
|
|
8
15
|
export type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
JirenHttpConfig,
|
|
17
|
+
ParsedUrl,
|
|
18
|
+
WarmupUrlConfig,
|
|
19
|
+
UrlRequestOptions,
|
|
20
|
+
UrlEndpoint,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
// Remove broken exports
|