jiren 1.2.6 → 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.
- package/components/client-node-native.ts +29 -0
- package/components/client.ts +185 -55
- package/components/types.ts +18 -20
- package/dist/index-node.js +569 -0
- package/dist/index.js +664 -0
- package/package.json +8 -6
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.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"author": "",
|
|
5
|
-
"main": "index.
|
|
6
|
-
"module": "index.
|
|
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.
|
|
47
|
-
"default": "./index-node.
|
|
48
|
+
"bun": "./dist/index.js",
|
|
49
|
+
"default": "./dist/index-node.js"
|
|
48
50
|
},
|
|
49
51
|
"./package.json": "./package.json"
|
|
50
52
|
},
|