jiren 1.2.7 → 1.2.8

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.
@@ -0,0 +1,569 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // components/native-node.ts
5
+ import koffi from "koffi";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ var __filename2 = fileURLToPath(import.meta.url);
9
+ var __dirname2 = path.dirname(__filename2);
10
+ var platform = process.platform;
11
+ var suffix = "so";
12
+ if (platform === "darwin") {
13
+ suffix = "dylib";
14
+ } else if (platform === "win32") {
15
+ suffix = "dll";
16
+ }
17
+ var libPath = path.join(__dirname2, `../lib/libhttpclient.${suffix}`);
18
+ var lib = koffi.load(libPath);
19
+ var symbols = {
20
+ zclient_new: lib.func("zclient_new", "void *", []),
21
+ zclient_free: lib.func("zclient_free", "void", ["void *"]),
22
+ zclient_get: lib.func("zclient_get", "void *", ["void *", "const char *"]),
23
+ zclient_post: lib.func("zclient_post", "void *", [
24
+ "void *",
25
+ "const char *",
26
+ "const char *"
27
+ ]),
28
+ zclient_request: lib.func("zclient_request", "void *", [
29
+ "void *",
30
+ "const char *",
31
+ "const char *",
32
+ "const char *",
33
+ "const char *",
34
+ "uint8_t",
35
+ "bool"
36
+ ]),
37
+ zclient_prefetch: lib.func("zclient_prefetch", "void", [
38
+ "void *",
39
+ "const char *"
40
+ ]),
41
+ zclient_response_status: lib.func("zclient_response_status", "uint16_t", [
42
+ "void *"
43
+ ]),
44
+ zclient_response_body: lib.func("zclient_response_body", "void *", [
45
+ "void *"
46
+ ]),
47
+ zclient_response_body_len: lib.func("zclient_response_body_len", "uint64_t", [
48
+ "void *"
49
+ ]),
50
+ zclient_response_headers: lib.func("zclient_response_headers", "void *", [
51
+ "void *"
52
+ ]),
53
+ zclient_response_headers_len: lib.func("zclient_response_headers_len", "uint64_t", ["void *"]),
54
+ zclient_response_parse_header_offsets: lib.func("zclient_response_parse_header_offsets", "void *", ["void *"]),
55
+ zclient_header_offsets_free: lib.func("zclient_header_offsets_free", "void", [
56
+ "void *"
57
+ ]),
58
+ z_find_header_value: lib.func("z_find_header_value", "void *", [
59
+ "void *",
60
+ "uint64_t",
61
+ "const char *"
62
+ ]),
63
+ zclient_header_value_free: lib.func("zclient_header_value_free", "void", [
64
+ "void *"
65
+ ]),
66
+ zclient_response_free: lib.func("zclient_response_free", "void", ["void *"]),
67
+ zclient_set_benchmark_mode: lib.func("zclient_set_benchmark_mode", "void", [
68
+ "void *",
69
+ "bool"
70
+ ])
71
+ };
72
+ var nativeLib = {
73
+ symbols
74
+ };
75
+
76
+ // components/cache.ts
77
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
78
+ import { gzipSync, gunzipSync } from "zlib";
79
+ import { createHash } from "crypto";
80
+ import { join } from "path";
81
+
82
+ class ResponseCache {
83
+ cacheDir;
84
+ maxSize;
85
+ constructor(maxSize = 100, cacheDir = ".cache/jiren") {
86
+ this.maxSize = maxSize;
87
+ this.cacheDir = cacheDir;
88
+ if (!existsSync(this.cacheDir)) {
89
+ mkdirSync(this.cacheDir, { recursive: true });
90
+ }
91
+ }
92
+ generateKey(url, path2, options) {
93
+ const fullUrl = path2 ? `${url}${path2}` : url;
94
+ const method = options?.method || "GET";
95
+ const headers = JSON.stringify(options?.headers || {});
96
+ const key = `${method}:${fullUrl}:${headers}`;
97
+ return createHash("md5").update(key).digest("hex");
98
+ }
99
+ getCacheFilePath(key) {
100
+ return join(this.cacheDir, `${key}.json.gz`);
101
+ }
102
+ get(url, path2, options) {
103
+ const key = this.generateKey(url, path2, options);
104
+ const filePath = this.getCacheFilePath(key);
105
+ if (!existsSync(filePath))
106
+ return null;
107
+ try {
108
+ const compressed = readFileSync(filePath);
109
+ const decompressed = gunzipSync(compressed);
110
+ const data = decompressed.toString("utf-8");
111
+ const entry = JSON.parse(data);
112
+ const now = Date.now();
113
+ if (now - entry.timestamp > entry.ttl) {
114
+ try {
115
+ __require("fs").unlinkSync(filePath);
116
+ } catch {}
117
+ return null;
118
+ }
119
+ return entry.response;
120
+ } catch (error) {
121
+ try {
122
+ __require("fs").unlinkSync(filePath);
123
+ } catch {}
124
+ return null;
125
+ }
126
+ }
127
+ set(url, response, ttl, path2, options) {
128
+ const key = this.generateKey(url, path2, options);
129
+ const filePath = this.getCacheFilePath(key);
130
+ const entry = {
131
+ response,
132
+ timestamp: Date.now(),
133
+ ttl
134
+ };
135
+ try {
136
+ const json = JSON.stringify(entry);
137
+ const compressed = gzipSync(json);
138
+ writeFileSync(filePath, compressed);
139
+ } catch (error) {
140
+ console.warn("Failed to write cache:", error);
141
+ }
142
+ }
143
+ clear(url) {
144
+ if (url) {
145
+ this.clearAll();
146
+ } else {
147
+ this.clearAll();
148
+ }
149
+ }
150
+ clearAll() {
151
+ try {
152
+ const fs = __require("fs");
153
+ const files = fs.readdirSync(this.cacheDir);
154
+ for (const file of files) {
155
+ if (file.endsWith(".json.gz")) {
156
+ fs.unlinkSync(join(this.cacheDir, file));
157
+ }
158
+ }
159
+ } catch (error) {}
160
+ }
161
+ stats() {
162
+ try {
163
+ const fs = __require("fs");
164
+ const files = fs.readdirSync(this.cacheDir);
165
+ const cacheFiles = files.filter((f) => f.endsWith(".json.gz"));
166
+ let totalSize = 0;
167
+ for (const file of cacheFiles) {
168
+ const stats = fs.statSync(join(this.cacheDir, file));
169
+ totalSize += stats.size;
170
+ }
171
+ return {
172
+ size: cacheFiles.length,
173
+ maxSize: this.maxSize,
174
+ cacheDir: this.cacheDir,
175
+ totalSizeKB: (totalSize / 1024).toFixed(2)
176
+ };
177
+ } catch {
178
+ return {
179
+ size: 0,
180
+ maxSize: this.maxSize,
181
+ cacheDir: this.cacheDir,
182
+ totalSizeKB: "0"
183
+ };
184
+ }
185
+ }
186
+ }
187
+
188
+ // components/client-node-native.ts
189
+ import koffi2 from "koffi";
190
+ import zlib from "zlib";
191
+ var STATUS_TEXT = {
192
+ 200: "OK",
193
+ 201: "Created",
194
+ 204: "No Content",
195
+ 301: "Moved Permanently",
196
+ 302: "Found",
197
+ 400: "Bad Request",
198
+ 401: "Unauthorized",
199
+ 403: "Forbidden",
200
+ 404: "Not Found",
201
+ 500: "Internal Server Error",
202
+ 502: "Bad Gateway",
203
+ 503: "Service Unavailable"
204
+ };
205
+ class JirenClient {
206
+ ptr = null;
207
+ urlMap = new Map;
208
+ cacheConfig = new Map;
209
+ cache;
210
+ url;
211
+ constructor(options) {
212
+ this.ptr = nativeLib.symbols.zclient_new();
213
+ if (!this.ptr)
214
+ throw new Error("Failed to create native client instance");
215
+ this.cache = new ResponseCache(100);
216
+ if (options?.benchmark) {
217
+ nativeLib.symbols.zclient_set_benchmark_mode(this.ptr, true);
218
+ }
219
+ if (options?.warmup) {
220
+ const urls = [];
221
+ const warmup = options.warmup;
222
+ if (Array.isArray(warmup)) {
223
+ for (const item of warmup) {
224
+ if (typeof item === "string") {
225
+ urls.push(item);
226
+ } else {
227
+ const config = item;
228
+ urls.push(config.url);
229
+ this.urlMap.set(config.key, config.url);
230
+ if (config.cache) {
231
+ const cacheConfig = typeof config.cache === "boolean" ? { enabled: true, ttl: 60000 } : { enabled: true, ttl: config.cache.ttl || 60000 };
232
+ this.cacheConfig.set(config.key, cacheConfig);
233
+ }
234
+ }
235
+ }
236
+ } else {
237
+ for (const [key, urlConfig] of Object.entries(warmup)) {
238
+ if (typeof urlConfig === "string") {
239
+ urls.push(urlConfig);
240
+ this.urlMap.set(key, urlConfig);
241
+ } else {
242
+ urls.push(urlConfig.url);
243
+ this.urlMap.set(key, urlConfig.url);
244
+ if (urlConfig.cache) {
245
+ const cacheConfig = typeof urlConfig.cache === "boolean" ? { enabled: true, ttl: 60000 } : { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
246
+ this.cacheConfig.set(key, cacheConfig);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ if (urls.length > 0) {
252
+ this.warmup(urls);
253
+ }
254
+ }
255
+ this.url = this.createUrlAccessor();
256
+ }
257
+ createUrlAccessor() {
258
+ const self = this;
259
+ return new Proxy({}, {
260
+ get(_target, prop) {
261
+ const baseUrl = self.urlMap.get(prop);
262
+ if (!baseUrl) {
263
+ throw new Error(`URL key "${prop}" not found. Available keys: ${Array.from(self.urlMap.keys()).join(", ")}`);
264
+ }
265
+ const buildUrl = (path2) => path2 ? `${baseUrl.replace(/\/$/, "")}/${path2.replace(/^\//, "")}` : baseUrl;
266
+ return {
267
+ get: async (options) => {
268
+ const cacheConfig = self.cacheConfig.get(prop);
269
+ if (cacheConfig?.enabled) {
270
+ const cached = self.cache.get(baseUrl, options?.path, options);
271
+ if (cached) {
272
+ return cached;
273
+ }
274
+ }
275
+ const response = await self.request("GET", buildUrl(options?.path), null, {
276
+ headers: options?.headers,
277
+ maxRedirects: options?.maxRedirects,
278
+ responseType: options?.responseType,
279
+ antibot: options?.antibot
280
+ });
281
+ if (cacheConfig?.enabled && typeof response === "object" && "status" in response) {
282
+ self.cache.set(baseUrl, response, cacheConfig.ttl, options?.path, options);
283
+ }
284
+ return response;
285
+ },
286
+ post: async (body, options) => {
287
+ return self.request("POST", buildUrl(options?.path), body || null, {
288
+ headers: options?.headers,
289
+ maxRedirects: options?.maxRedirects,
290
+ responseType: options?.responseType
291
+ });
292
+ },
293
+ put: async (body, options) => {
294
+ return self.request("PUT", buildUrl(options?.path), body || null, {
295
+ headers: options?.headers,
296
+ maxRedirects: options?.maxRedirects,
297
+ responseType: options?.responseType
298
+ });
299
+ },
300
+ patch: async (body, options) => {
301
+ return self.request("PATCH", buildUrl(options?.path), body || null, {
302
+ headers: options?.headers,
303
+ maxRedirects: options?.maxRedirects,
304
+ responseType: options?.responseType
305
+ });
306
+ },
307
+ delete: async (body, options) => {
308
+ return self.request("DELETE", buildUrl(options?.path), body || null, {
309
+ headers: options?.headers,
310
+ maxRedirects: options?.maxRedirects,
311
+ responseType: options?.responseType
312
+ });
313
+ },
314
+ head: async (options) => {
315
+ return self.request("HEAD", buildUrl(options?.path), null, {
316
+ headers: options?.headers,
317
+ maxRedirects: options?.maxRedirects,
318
+ antibot: options?.antibot
319
+ });
320
+ },
321
+ options: async (options) => {
322
+ return self.request("OPTIONS", buildUrl(options?.path), null, {
323
+ headers: options?.headers,
324
+ maxRedirects: options?.maxRedirects,
325
+ antibot: options?.antibot
326
+ });
327
+ },
328
+ prefetch: async (options) => {
329
+ self.cache.clear(baseUrl);
330
+ const cacheConfig = self.cacheConfig.get(prop);
331
+ if (cacheConfig?.enabled) {
332
+ await self.request("GET", buildUrl(options?.path), null, {
333
+ headers: options?.headers,
334
+ maxRedirects: options?.maxRedirects,
335
+ antibot: options?.antibot
336
+ });
337
+ }
338
+ }
339
+ };
340
+ }
341
+ });
342
+ }
343
+ close() {
344
+ if (this.ptr) {
345
+ nativeLib.symbols.zclient_free(this.ptr);
346
+ this.ptr = null;
347
+ }
348
+ }
349
+ async warmup(urls) {
350
+ if (!this.ptr)
351
+ throw new Error("Client is closed");
352
+ await Promise.all(urls.map((url) => new Promise((resolve) => {
353
+ const urlBuffer = Buffer.from(url + "\x00");
354
+ nativeLib.symbols.zclient_prefetch(this.ptr, url);
355
+ resolve();
356
+ })));
357
+ }
358
+ prefetch(urls) {
359
+ this.warmup(urls);
360
+ }
361
+ async request(method, url, body, options) {
362
+ if (!this.ptr)
363
+ throw new Error("Client is closed");
364
+ let headers = {};
365
+ let maxRedirects = 5;
366
+ let responseType;
367
+ let antibot = false;
368
+ if (options) {
369
+ if ("maxRedirects" in options || "headers" in options || "responseType" in options || "method" in options || "timeout" in options || "antibot" in options) {
370
+ const opts = options;
371
+ if (opts.headers)
372
+ headers = opts.headers;
373
+ if (opts.maxRedirects !== undefined)
374
+ maxRedirects = opts.maxRedirects;
375
+ if (opts.responseType)
376
+ responseType = opts.responseType;
377
+ if (opts.antibot !== undefined)
378
+ antibot = opts.antibot;
379
+ } else {
380
+ headers = options;
381
+ }
382
+ }
383
+ const defaultHeaders = {
384
+ "user-agent": "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",
385
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
386
+ "accept-encoding": "gzip",
387
+ "accept-language": "en-US,en;q=0.9",
388
+ "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
389
+ "sec-ch-ua-mobile": "?0",
390
+ "sec-ch-ua-platform": '"macOS"',
391
+ "sec-fetch-dest": "document",
392
+ "sec-fetch-mode": "navigate",
393
+ "sec-fetch-site": "none",
394
+ "sec-fetch-user": "?1",
395
+ "upgrade-insecure-requests": "1"
396
+ };
397
+ const finalHeaders = { ...defaultHeaders, ...headers };
398
+ const orderedHeaders = {};
399
+ const keys = [
400
+ "sec-ch-ua",
401
+ "sec-ch-ua-mobile",
402
+ "sec-ch-ua-platform",
403
+ "upgrade-insecure-requests",
404
+ "user-agent",
405
+ "accept",
406
+ "sec-fetch-site",
407
+ "sec-fetch-mode",
408
+ "sec-fetch-user",
409
+ "sec-fetch-dest",
410
+ "accept-encoding",
411
+ "accept-language"
412
+ ];
413
+ for (const key of keys) {
414
+ if (finalHeaders[key]) {
415
+ orderedHeaders[key] = finalHeaders[key];
416
+ delete finalHeaders[key];
417
+ }
418
+ }
419
+ for (const [key, value] of Object.entries(finalHeaders)) {
420
+ orderedHeaders[key] = value;
421
+ }
422
+ const headerStr = Object.entries(orderedHeaders).map(([k, v]) => `${k.toLowerCase()}: ${v}`).join(`\r
423
+ `);
424
+ const respPtr = nativeLib.symbols.zclient_request(this.ptr, method, url, headerStr.length > 0 ? headerStr : null, body || null, maxRedirects, antibot);
425
+ const response = this.parseResponse(respPtr, url);
426
+ if (responseType) {
427
+ if (responseType === "json")
428
+ return response.body.json();
429
+ if (responseType === "text")
430
+ return response.body.text();
431
+ if (responseType === "arraybuffer")
432
+ return response.body.arrayBuffer();
433
+ if (responseType === "blob")
434
+ return response.body.blob();
435
+ }
436
+ return response;
437
+ }
438
+ parseResponse(respPtr, url) {
439
+ if (!respPtr)
440
+ throw new Error("Native request failed (returned null pointer)");
441
+ try {
442
+ const status = nativeLib.symbols.zclient_response_status(respPtr);
443
+ const len = Number(nativeLib.symbols.zclient_response_body_len(respPtr));
444
+ const bodyPtr = nativeLib.symbols.zclient_response_body(respPtr);
445
+ const headersLen = Number(nativeLib.symbols.zclient_response_headers_len(respPtr));
446
+ let headersObj = {};
447
+ if (headersLen > 0) {
448
+ const rawHeadersPtr = nativeLib.symbols.zclient_response_headers(respPtr);
449
+ if (rawHeadersPtr) {
450
+ const raw = Buffer.from(koffi2.decode(rawHeadersPtr, "uint8_t", headersLen));
451
+ headersObj = new NativeHeaders(raw);
452
+ }
453
+ }
454
+ const headersProxy = new Proxy(headersObj instanceof NativeHeaders ? headersObj : {}, {
455
+ get(target, prop) {
456
+ if (target instanceof NativeHeaders && typeof prop === "string") {
457
+ if (prop === "toJSON")
458
+ return () => target.toJSON();
459
+ const val = target.get(prop);
460
+ if (val !== null)
461
+ return val;
462
+ }
463
+ return Reflect.get(target, prop);
464
+ }
465
+ });
466
+ let buffer = Buffer.alloc(0);
467
+ if (len > 0 && bodyPtr) {
468
+ buffer = Buffer.from(koffi2.decode(bodyPtr, "uint8_t", len));
469
+ const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
470
+ if (len > 0) {
471
+ console.log(`[Jiren] Body len: ${len}, Encoding: ${contentEncoding}`);
472
+ if (buffer.length > 2) {
473
+ console.log(`[Jiren] Bytes: 0x${buffer[0]?.toString(16)} 0x${buffer[1]?.toString(16)} 0x${buffer[2]?.toString(16)}`);
474
+ }
475
+ }
476
+ if (contentEncoding === "gzip" || buffer.length > 2 && buffer[0] === 31 && buffer[1] === 139) {
477
+ try {
478
+ console.log("[Jiren] Attempting gunzip...");
479
+ buffer = zlib.gunzipSync(buffer);
480
+ console.log("[Jiren] Gunzip success!");
481
+ } catch (e) {
482
+ console.warn("Failed to gunzip response body:", e);
483
+ }
484
+ }
485
+ }
486
+ let bodyUsed = false;
487
+ const consumeBody = () => {
488
+ bodyUsed = true;
489
+ };
490
+ const bodyObj = {
491
+ bodyUsed: false,
492
+ arrayBuffer: async () => {
493
+ consumeBody();
494
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
495
+ },
496
+ blob: async () => {
497
+ consumeBody();
498
+ return new Blob([buffer]);
499
+ },
500
+ text: async () => {
501
+ consumeBody();
502
+ return buffer.toString("utf-8");
503
+ },
504
+ json: async () => {
505
+ consumeBody();
506
+ return JSON.parse(buffer.toString("utf-8"));
507
+ }
508
+ };
509
+ Object.defineProperty(bodyObj, "bodyUsed", {
510
+ get: () => bodyUsed
511
+ });
512
+ return {
513
+ url,
514
+ status,
515
+ statusText: STATUS_TEXT[status] || "",
516
+ headers: headersProxy,
517
+ ok: status >= 200 && status < 300,
518
+ redirected: false,
519
+ type: "basic",
520
+ body: bodyObj
521
+ };
522
+ } finally {
523
+ nativeLib.symbols.zclient_response_free(respPtr);
524
+ }
525
+ }
526
+ }
527
+
528
+ class NativeHeaders {
529
+ raw;
530
+ len;
531
+ cache = new Map;
532
+ parsed = false;
533
+ constructor(raw) {
534
+ this.raw = raw;
535
+ this.len = raw.length;
536
+ }
537
+ ensureParsed() {
538
+ if (this.parsed)
539
+ return;
540
+ try {
541
+ const text = this.raw.toString("utf-8");
542
+ const lines = text.split(`\r
543
+ `);
544
+ for (const line of lines) {
545
+ if (!line)
546
+ continue;
547
+ const colon = line.indexOf(":");
548
+ if (colon === -1)
549
+ continue;
550
+ const key = line.substring(0, colon).trim().toLowerCase();
551
+ const val = line.substring(colon + 1).trim();
552
+ this.cache.set(key, val);
553
+ }
554
+ } catch (e) {}
555
+ this.parsed = true;
556
+ }
557
+ get(name) {
558
+ const target = name.toLowerCase();
559
+ this.ensureParsed();
560
+ return this.cache.get(target) || null;
561
+ }
562
+ toJSON() {
563
+ this.ensureParsed();
564
+ return Object.fromEntries(this.cache);
565
+ }
566
+ }
567
+ export {
568
+ JirenClient
569
+ };
package/dist/index.js ADDED
@@ -0,0 +1,664 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // components/client.ts
5
+ import { toArrayBuffer } from "bun:ffi";
6
+
7
+ // components/native.ts
8
+ import { dlopen, FFIType, suffix } from "bun:ffi";
9
+ import { join } from "path";
10
+ var libPath = join(import.meta.dir, `../lib/libhttpclient.${suffix}`);
11
+ var ffiDef = {
12
+ zclient_new: {
13
+ args: [],
14
+ returns: FFIType.ptr
15
+ },
16
+ zclient_free: {
17
+ args: [FFIType.ptr],
18
+ returns: FFIType.void
19
+ },
20
+ zclient_get: {
21
+ args: [FFIType.ptr, FFIType.cstring],
22
+ returns: FFIType.ptr
23
+ },
24
+ zclient_post: {
25
+ args: [FFIType.ptr, FFIType.cstring, FFIType.cstring],
26
+ returns: FFIType.ptr
27
+ },
28
+ zclient_request: {
29
+ args: [
30
+ FFIType.ptr,
31
+ FFIType.cstring,
32
+ FFIType.cstring,
33
+ FFIType.cstring,
34
+ FFIType.cstring,
35
+ FFIType.u8,
36
+ FFIType.bool
37
+ ],
38
+ returns: FFIType.ptr
39
+ },
40
+ zclient_prefetch: {
41
+ args: [FFIType.ptr, FFIType.cstring],
42
+ returns: FFIType.void
43
+ },
44
+ zclient_response_status: {
45
+ args: [FFIType.ptr],
46
+ returns: FFIType.u16
47
+ },
48
+ zclient_response_body: {
49
+ args: [FFIType.ptr],
50
+ returns: FFIType.ptr
51
+ },
52
+ zclient_response_body_len: {
53
+ args: [FFIType.ptr],
54
+ returns: FFIType.u64
55
+ },
56
+ zclient_response_headers: {
57
+ args: [FFIType.ptr],
58
+ returns: FFIType.ptr
59
+ },
60
+ zclient_response_headers_len: {
61
+ args: [FFIType.ptr],
62
+ returns: FFIType.u64
63
+ },
64
+ zclient_response_parse_header_offsets: {
65
+ args: [FFIType.ptr],
66
+ returns: FFIType.ptr
67
+ },
68
+ zclient_header_offsets_free: {
69
+ args: [FFIType.ptr],
70
+ returns: FFIType.void
71
+ },
72
+ z_find_header_value: {
73
+ args: [FFIType.ptr, FFIType.u64, FFIType.cstring],
74
+ returns: FFIType.ptr
75
+ },
76
+ zclient_header_value_free: {
77
+ args: [FFIType.ptr],
78
+ returns: FFIType.void
79
+ },
80
+ zclient_response_free: {
81
+ args: [FFIType.ptr],
82
+ returns: FFIType.void
83
+ },
84
+ zclient_set_benchmark_mode: {
85
+ args: [FFIType.ptr, FFIType.bool],
86
+ returns: FFIType.void
87
+ }
88
+ };
89
+ var lib = dlopen(libPath, ffiDef);
90
+
91
+ // components/cache.ts
92
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
93
+ import { gzipSync, gunzipSync } from "zlib";
94
+ import { createHash } from "crypto";
95
+ import { join as join2 } from "path";
96
+
97
+ class ResponseCache {
98
+ cacheDir;
99
+ maxSize;
100
+ constructor(maxSize = 100, cacheDir = ".cache/jiren") {
101
+ this.maxSize = maxSize;
102
+ this.cacheDir = cacheDir;
103
+ if (!existsSync(this.cacheDir)) {
104
+ mkdirSync(this.cacheDir, { recursive: true });
105
+ }
106
+ }
107
+ generateKey(url, path, options) {
108
+ const fullUrl = path ? `${url}${path}` : url;
109
+ const method = options?.method || "GET";
110
+ const headers = JSON.stringify(options?.headers || {});
111
+ const key = `${method}:${fullUrl}:${headers}`;
112
+ return createHash("md5").update(key).digest("hex");
113
+ }
114
+ getCacheFilePath(key) {
115
+ return join2(this.cacheDir, `${key}.json.gz`);
116
+ }
117
+ get(url, path, options) {
118
+ const key = this.generateKey(url, path, options);
119
+ const filePath = this.getCacheFilePath(key);
120
+ if (!existsSync(filePath))
121
+ return null;
122
+ try {
123
+ const compressed = readFileSync(filePath);
124
+ const decompressed = gunzipSync(compressed);
125
+ const data = decompressed.toString("utf-8");
126
+ const entry = JSON.parse(data);
127
+ const now = Date.now();
128
+ if (now - entry.timestamp > entry.ttl) {
129
+ try {
130
+ __require("fs").unlinkSync(filePath);
131
+ } catch {}
132
+ return null;
133
+ }
134
+ return entry.response;
135
+ } catch (error) {
136
+ try {
137
+ __require("fs").unlinkSync(filePath);
138
+ } catch {}
139
+ return null;
140
+ }
141
+ }
142
+ set(url, response, ttl, path, options) {
143
+ const key = this.generateKey(url, path, options);
144
+ const filePath = this.getCacheFilePath(key);
145
+ const entry = {
146
+ response,
147
+ timestamp: Date.now(),
148
+ ttl
149
+ };
150
+ try {
151
+ const json = JSON.stringify(entry);
152
+ const compressed = gzipSync(json);
153
+ writeFileSync(filePath, compressed);
154
+ } catch (error) {
155
+ console.warn("Failed to write cache:", error);
156
+ }
157
+ }
158
+ clear(url) {
159
+ if (url) {
160
+ this.clearAll();
161
+ } else {
162
+ this.clearAll();
163
+ }
164
+ }
165
+ clearAll() {
166
+ try {
167
+ const fs = __require("fs");
168
+ const files = fs.readdirSync(this.cacheDir);
169
+ for (const file of files) {
170
+ if (file.endsWith(".json.gz")) {
171
+ fs.unlinkSync(join2(this.cacheDir, file));
172
+ }
173
+ }
174
+ } catch (error) {}
175
+ }
176
+ stats() {
177
+ try {
178
+ const fs = __require("fs");
179
+ const files = fs.readdirSync(this.cacheDir);
180
+ const cacheFiles = files.filter((f) => f.endsWith(".json.gz"));
181
+ let totalSize = 0;
182
+ for (const file of cacheFiles) {
183
+ const stats = fs.statSync(join2(this.cacheDir, file));
184
+ totalSize += stats.size;
185
+ }
186
+ return {
187
+ size: cacheFiles.length,
188
+ maxSize: this.maxSize,
189
+ cacheDir: this.cacheDir,
190
+ totalSizeKB: (totalSize / 1024).toFixed(2)
191
+ };
192
+ } catch {
193
+ return {
194
+ size: 0,
195
+ maxSize: this.maxSize,
196
+ cacheDir: this.cacheDir,
197
+ totalSizeKB: "0"
198
+ };
199
+ }
200
+ }
201
+ }
202
+
203
+ // components/client.ts
204
+ var STATUS_TEXT = {
205
+ 200: "OK",
206
+ 201: "Created",
207
+ 204: "No Content",
208
+ 301: "Moved Permanently",
209
+ 302: "Found",
210
+ 400: "Bad Request",
211
+ 401: "Unauthorized",
212
+ 403: "Forbidden",
213
+ 404: "Not Found",
214
+ 500: "Internal Server Error",
215
+ 502: "Bad Gateway",
216
+ 503: "Service Unavailable"
217
+ };
218
+ class JirenClient {
219
+ ptr;
220
+ urlMap = new Map;
221
+ cacheConfig = new Map;
222
+ cache;
223
+ inflightRequests = new Map;
224
+ globalRetry;
225
+ url;
226
+ constructor(options) {
227
+ this.ptr = lib.symbols.zclient_new();
228
+ if (!this.ptr)
229
+ throw new Error("Failed to create native client instance");
230
+ this.cache = new ResponseCache(100);
231
+ if (options?.benchmark) {
232
+ lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
233
+ }
234
+ if (options?.warmup) {
235
+ const urls = [];
236
+ const warmup = options.warmup;
237
+ if (Array.isArray(warmup)) {
238
+ for (const item of warmup) {
239
+ if (typeof item === "string") {
240
+ urls.push(item);
241
+ } else {
242
+ const config = item;
243
+ urls.push(config.url);
244
+ this.urlMap.set(config.key, config.url);
245
+ if (config.cache) {
246
+ const cacheConfig = typeof config.cache === "boolean" ? { enabled: true, ttl: 60000 } : { enabled: true, ttl: config.cache.ttl || 60000 };
247
+ this.cacheConfig.set(config.key, cacheConfig);
248
+ }
249
+ }
250
+ }
251
+ } else {
252
+ for (const [key, urlConfig] of Object.entries(warmup)) {
253
+ if (typeof urlConfig === "string") {
254
+ urls.push(urlConfig);
255
+ this.urlMap.set(key, urlConfig);
256
+ } else {
257
+ urls.push(urlConfig.url);
258
+ this.urlMap.set(key, urlConfig.url);
259
+ if (urlConfig.cache) {
260
+ const cacheConfig = typeof urlConfig.cache === "boolean" ? { enabled: true, ttl: 60000 } : { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
261
+ this.cacheConfig.set(key, cacheConfig);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ if (urls.length > 0) {
267
+ this.warmup(urls);
268
+ }
269
+ }
270
+ this.url = this.createUrlAccessor();
271
+ if (options?.retry) {
272
+ this.globalRetry = typeof options.retry === "number" ? { count: options.retry, delay: 100, backoff: 2 } : options.retry;
273
+ }
274
+ }
275
+ async waitFor(ms) {
276
+ return new Promise((resolve) => setTimeout(resolve, ms));
277
+ }
278
+ createUrlAccessor() {
279
+ const self = this;
280
+ return new Proxy({}, {
281
+ get(_target, prop) {
282
+ const baseUrl = self.urlMap.get(prop);
283
+ if (!baseUrl) {
284
+ throw new Error(`URL key "${prop}" not found. Available keys: ${Array.from(self.urlMap.keys()).join(", ")}`);
285
+ }
286
+ const buildUrl = (path) => path ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}` : baseUrl;
287
+ return {
288
+ get: async (options) => {
289
+ const cacheConfig = self.cacheConfig.get(prop);
290
+ if (cacheConfig?.enabled) {
291
+ const cached = self.cache.get(baseUrl, options?.path, options);
292
+ if (cached) {
293
+ return cached;
294
+ }
295
+ }
296
+ const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(options?.headers || {})}`;
297
+ if (self.inflightRequests.has(dedupKey)) {
298
+ return self.inflightRequests.get(dedupKey);
299
+ }
300
+ const requestPromise = (async () => {
301
+ try {
302
+ const response = await self.request("GET", buildUrl(options?.path), null, {
303
+ headers: options?.headers,
304
+ maxRedirects: options?.maxRedirects,
305
+ responseType: options?.responseType,
306
+ antibot: options?.antibot
307
+ });
308
+ if (cacheConfig?.enabled && typeof response === "object" && "status" in response) {
309
+ self.cache.set(baseUrl, response, cacheConfig.ttl, options?.path, options);
310
+ }
311
+ return response;
312
+ } finally {
313
+ self.inflightRequests.delete(dedupKey);
314
+ }
315
+ })();
316
+ self.inflightRequests.set(dedupKey, requestPromise);
317
+ return requestPromise;
318
+ },
319
+ post: async (options) => {
320
+ const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
321
+ return self.request("POST", buildUrl(options?.path), serializedBody, {
322
+ headers,
323
+ maxRedirects: options?.maxRedirects,
324
+ responseType: options?.responseType
325
+ });
326
+ },
327
+ put: async (options) => {
328
+ const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
329
+ return self.request("PUT", buildUrl(options?.path), serializedBody, {
330
+ headers,
331
+ maxRedirects: options?.maxRedirects,
332
+ responseType: options?.responseType
333
+ });
334
+ },
335
+ patch: async (options) => {
336
+ const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
337
+ return self.request("PATCH", buildUrl(options?.path), serializedBody, {
338
+ headers,
339
+ maxRedirects: options?.maxRedirects,
340
+ responseType: options?.responseType
341
+ });
342
+ },
343
+ delete: async (options) => {
344
+ const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
345
+ return self.request("DELETE", buildUrl(options?.path), serializedBody, {
346
+ headers,
347
+ maxRedirects: options?.maxRedirects,
348
+ responseType: options?.responseType
349
+ });
350
+ },
351
+ head: async (options) => {
352
+ return self.request("HEAD", buildUrl(options?.path), null, {
353
+ headers: options?.headers,
354
+ maxRedirects: options?.maxRedirects,
355
+ antibot: options?.antibot
356
+ });
357
+ },
358
+ options: async (options) => {
359
+ return self.request("OPTIONS", buildUrl(options?.path), null, {
360
+ headers: options?.headers,
361
+ maxRedirects: options?.maxRedirects,
362
+ antibot: options?.antibot
363
+ });
364
+ },
365
+ prefetch: async (options) => {
366
+ self.cache.clear(baseUrl);
367
+ const cacheConfig = self.cacheConfig.get(prop);
368
+ if (cacheConfig?.enabled) {
369
+ await self.request("GET", buildUrl(options?.path), null, {
370
+ headers: options?.headers,
371
+ maxRedirects: options?.maxRedirects,
372
+ antibot: options?.antibot
373
+ });
374
+ }
375
+ }
376
+ };
377
+ }
378
+ });
379
+ }
380
+ close() {
381
+ if (this.ptr) {
382
+ lib.symbols.zclient_free(this.ptr);
383
+ this.ptr = null;
384
+ }
385
+ }
386
+ async warmup(urls) {
387
+ if (!this.ptr)
388
+ throw new Error("Client is closed");
389
+ await Promise.all(urls.map((url) => new Promise((resolve) => {
390
+ const urlBuffer = Buffer.from(url + "\x00");
391
+ lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
392
+ resolve();
393
+ })));
394
+ }
395
+ prefetch(urls) {
396
+ this.warmup(urls);
397
+ }
398
+ async request(method, url, body, options) {
399
+ if (!this.ptr)
400
+ throw new Error("Client is closed");
401
+ let headers = {};
402
+ let maxRedirects = 5;
403
+ let responseType;
404
+ let antibot = false;
405
+ if (options) {
406
+ if ("maxRedirects" in options || "headers" in options || "responseType" in options || "method" in options || "timeout" in options || "antibot" in options) {
407
+ const opts = options;
408
+ if (opts.headers)
409
+ headers = opts.headers;
410
+ if (opts.maxRedirects !== undefined)
411
+ maxRedirects = opts.maxRedirects;
412
+ if (opts.responseType)
413
+ responseType = opts.responseType;
414
+ if (opts.antibot !== undefined)
415
+ antibot = opts.antibot;
416
+ } else {
417
+ headers = options;
418
+ }
419
+ }
420
+ const methodBuffer = Buffer.from(method + "\x00");
421
+ const urlBuffer = Buffer.from(url + "\x00");
422
+ let bodyBuffer = null;
423
+ if (body) {
424
+ bodyBuffer = Buffer.from(body + "\x00");
425
+ }
426
+ let headersBuffer = null;
427
+ const defaultHeaders = {
428
+ "user-agent": "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",
429
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
430
+ "accept-encoding": "gzip",
431
+ "accept-language": "en-US,en;q=0.9",
432
+ "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
433
+ "sec-ch-ua-mobile": "?0",
434
+ "sec-ch-ua-platform": '"macOS"',
435
+ "sec-fetch-dest": "document",
436
+ "sec-fetch-mode": "navigate",
437
+ "sec-fetch-site": "none",
438
+ "sec-fetch-user": "?1",
439
+ "upgrade-insecure-requests": "1"
440
+ };
441
+ const finalHeaders = { ...defaultHeaders, ...headers };
442
+ const orderedHeaders = {};
443
+ const keys = [
444
+ "sec-ch-ua",
445
+ "sec-ch-ua-mobile",
446
+ "sec-ch-ua-platform",
447
+ "upgrade-insecure-requests",
448
+ "user-agent",
449
+ "accept",
450
+ "sec-fetch-site",
451
+ "sec-fetch-mode",
452
+ "sec-fetch-user",
453
+ "sec-fetch-dest",
454
+ "accept-encoding",
455
+ "accept-language"
456
+ ];
457
+ for (const key of keys) {
458
+ if (finalHeaders[key]) {
459
+ orderedHeaders[key] = finalHeaders[key];
460
+ delete finalHeaders[key];
461
+ }
462
+ }
463
+ for (const [key, value] of Object.entries(finalHeaders)) {
464
+ orderedHeaders[key] = value;
465
+ }
466
+ const headerStr = Object.entries(orderedHeaders).map(([k, v]) => `${k.toLowerCase()}: ${v}`).join(`\r
467
+ `);
468
+ if (headerStr.length > 0) {
469
+ headersBuffer = Buffer.from(headerStr + "\x00");
470
+ }
471
+ let retryConfig = this.globalRetry;
472
+ if (options && typeof options === "object" && "retry" in options) {
473
+ const userRetry = options.retry;
474
+ if (typeof userRetry === "number") {
475
+ retryConfig = { count: userRetry, delay: 100, backoff: 2 };
476
+ } else if (typeof userRetry === "object") {
477
+ retryConfig = userRetry;
478
+ }
479
+ }
480
+ let attempts = 0;
481
+ const maxAttempts = (retryConfig?.count || 0) + 1;
482
+ let currentDelay = retryConfig?.delay || 100;
483
+ const backoff = retryConfig?.backoff || 2;
484
+ let lastError;
485
+ while (attempts < maxAttempts) {
486
+ attempts++;
487
+ try {
488
+ const respPtr = lib.symbols.zclient_request(this.ptr, methodBuffer, urlBuffer, headersBuffer, bodyBuffer, maxRedirects, antibot);
489
+ if (!respPtr) {
490
+ throw new Error("Native request failed (returned null pointer)");
491
+ }
492
+ const response = this.parseResponse(respPtr, url);
493
+ if (responseType) {
494
+ if (responseType === "json")
495
+ return response.body.json();
496
+ if (responseType === "text")
497
+ return response.body.text();
498
+ if (responseType === "arraybuffer")
499
+ return response.body.arrayBuffer();
500
+ if (responseType === "blob")
501
+ return response.body.blob();
502
+ }
503
+ return response;
504
+ } catch (err) {
505
+ lastError = err;
506
+ if (attempts < maxAttempts) {
507
+ await this.waitFor(currentDelay);
508
+ currentDelay *= backoff;
509
+ }
510
+ }
511
+ }
512
+ throw lastError || new Error("Request failed after retries");
513
+ }
514
+ parseResponse(respPtr, url) {
515
+ if (!respPtr)
516
+ throw new Error("Native request failed (returned null pointer)");
517
+ try {
518
+ const status = lib.symbols.zclient_response_status(respPtr);
519
+ const len = Number(lib.symbols.zclient_response_body_len(respPtr));
520
+ const bodyPtr = lib.symbols.zclient_response_body(respPtr);
521
+ const headersLen = Number(lib.symbols.zclient_response_headers_len(respPtr));
522
+ let headersObj = {};
523
+ if (headersLen > 0) {
524
+ const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
525
+ if (rawHeadersPtr) {
526
+ const rawSrc = toArrayBuffer(rawHeadersPtr, 0, headersLen);
527
+ const raw = new Uint8Array(rawSrc.slice(0));
528
+ headersObj = new NativeHeaders(raw);
529
+ }
530
+ }
531
+ const headersProxy = new Proxy(headersObj instanceof NativeHeaders ? headersObj : {}, {
532
+ get(target, prop) {
533
+ if (target instanceof NativeHeaders && typeof prop === "string") {
534
+ if (prop === "toJSON")
535
+ return () => target.toJSON();
536
+ const val = target.get(prop);
537
+ if (val !== null)
538
+ return val;
539
+ }
540
+ return Reflect.get(target, prop);
541
+ }
542
+ });
543
+ let buffer = new ArrayBuffer(0);
544
+ if (len > 0 && bodyPtr) {
545
+ buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
546
+ }
547
+ let bodyUsed = false;
548
+ const consumeBody = () => {
549
+ if (bodyUsed) {}
550
+ bodyUsed = true;
551
+ };
552
+ const bodyObj = {
553
+ bodyUsed: false,
554
+ arrayBuffer: async () => {
555
+ consumeBody();
556
+ if (Buffer.isBuffer(buffer)) {
557
+ const buf = buffer;
558
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
559
+ }
560
+ return buffer;
561
+ },
562
+ blob: async () => {
563
+ consumeBody();
564
+ return new Blob([buffer]);
565
+ },
566
+ text: async () => {
567
+ consumeBody();
568
+ return new TextDecoder().decode(buffer);
569
+ },
570
+ json: async () => {
571
+ consumeBody();
572
+ const text = new TextDecoder().decode(buffer);
573
+ return JSON.parse(text);
574
+ }
575
+ };
576
+ Object.defineProperty(bodyObj, "bodyUsed", {
577
+ get: () => bodyUsed
578
+ });
579
+ return {
580
+ url,
581
+ status,
582
+ statusText: STATUS_TEXT[status] || "",
583
+ headers: headersProxy,
584
+ ok: status >= 200 && status < 300,
585
+ redirected: false,
586
+ type: "basic",
587
+ body: bodyObj
588
+ };
589
+ } finally {
590
+ lib.symbols.zclient_response_free(respPtr);
591
+ }
592
+ }
593
+ prepareBody(body, userHeaders) {
594
+ let serializedBody = null;
595
+ const headers = { ...userHeaders };
596
+ if (body !== null && body !== undefined) {
597
+ if (typeof body === "object") {
598
+ serializedBody = JSON.stringify(body);
599
+ const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
600
+ if (!hasContentType) {
601
+ headers["Content-Type"] = "application/json";
602
+ }
603
+ } else {
604
+ serializedBody = String(body);
605
+ }
606
+ }
607
+ return { headers, serializedBody };
608
+ }
609
+ }
610
+
611
+ class NativeHeaders {
612
+ raw;
613
+ len;
614
+ decoder = new TextDecoder;
615
+ cache = new Map;
616
+ constructor(raw) {
617
+ this.raw = raw;
618
+ this.len = raw.byteLength;
619
+ }
620
+ get(name) {
621
+ const target = name.toLowerCase();
622
+ if (this.cache.has(target))
623
+ return this.cache.get(target);
624
+ const keyBuf = Buffer.from(target + "\x00");
625
+ const resPtr = lib.symbols.z_find_header_value(this.raw, this.len, keyBuf);
626
+ if (!resPtr)
627
+ return null;
628
+ try {
629
+ const view = new DataView(toArrayBuffer(resPtr, 0, 16));
630
+ const valPtr = view.getBigUint64(0, true);
631
+ const valLen = Number(view.getBigUint64(8, true));
632
+ if (valLen === 0) {
633
+ this.cache.set(target, "");
634
+ return "";
635
+ }
636
+ const valBytes = toArrayBuffer(Number(valPtr), 0, valLen);
637
+ const val = this.decoder.decode(valBytes);
638
+ this.cache.set(target, val);
639
+ return val;
640
+ } finally {
641
+ lib.symbols.zclient_header_value_free(resPtr);
642
+ }
643
+ }
644
+ toJSON() {
645
+ const obj = {};
646
+ const text = this.decoder.decode(this.raw);
647
+ const lines = text.split(`\r
648
+ `);
649
+ for (const line of lines) {
650
+ if (!line)
651
+ continue;
652
+ const colon = line.indexOf(":");
653
+ if (colon === -1)
654
+ continue;
655
+ const key = line.substring(0, colon).trim().toLowerCase();
656
+ const val = line.substring(colon + 1).trim();
657
+ obj[key] = val;
658
+ }
659
+ return obj;
660
+ }
661
+ }
662
+ export {
663
+ JirenClient
664
+ };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "author": "",
5
- "main": "index.ts",
6
- "module": "index.ts",
5
+ "main": "dist/index-node.js",
6
+ "module": "dist/index.js",
7
7
  "types": "index.ts",
8
8
  "devDependencies": {
9
9
  "@types/bun": "^1.3.4"
@@ -17,7 +17,8 @@
17
17
  "index-node.ts",
18
18
  "types",
19
19
  "lib",
20
- "components"
20
+ "components",
21
+ "dist"
21
22
  ],
22
23
  "keywords": [
23
24
  "http",
@@ -34,6 +35,7 @@
34
35
  "license": "MIT",
35
36
  "scripts": {
36
37
  "build:zig": "cd .. && zig build --release=fast",
38
+ "build": "bun build ./index.ts --outfile ./dist/index.js --target bun && bun build ./index-node.ts --outfile ./dist/index-node.js --target node --external koffi",
37
39
  "test": "bun run examples/basic.ts"
38
40
  },
39
41
  "engines": {
@@ -43,8 +45,8 @@
43
45
  "type": "module",
44
46
  "exports": {
45
47
  ".": {
46
- "bun": "./index.ts",
47
- "default": "./index-node.ts"
48
+ "bun": "./dist/index.js",
49
+ "default": "./dist/index-node.js"
48
50
  },
49
51
  "./package.json": "./package.json"
50
52
  },