jiren 1.5.5 → 1.6.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 +123 -313
- package/components/cache.ts +1 -1
- package/components/client-node-native.ts +602 -159
- package/components/client.ts +51 -29
- package/components/metrics.ts +1 -4
- package/components/native-node.ts +48 -3
- package/components/native.ts +29 -0
- package/components/persistent-worker.ts +73 -0
- package/components/subprocess-worker.ts +65 -0
- package/components/types.ts +2 -0
- package/components/worker-pool.ts +169 -0
- package/components/worker.ts +39 -23
- package/dist/components/cache.d.ts +76 -0
- package/dist/components/cache.d.ts.map +1 -0
- package/dist/components/cache.js +439 -0
- package/dist/components/cache.js.map +1 -0
- package/dist/components/client-node-native.d.ts +134 -0
- package/dist/components/client-node-native.d.ts.map +1 -0
- package/dist/components/client-node-native.js +811 -0
- package/dist/components/client-node-native.js.map +1 -0
- package/dist/components/metrics.d.ts +104 -0
- package/dist/components/metrics.d.ts.map +1 -0
- package/dist/components/metrics.js +296 -0
- package/dist/components/metrics.js.map +1 -0
- package/dist/components/native-node.d.ts +67 -0
- package/dist/components/native-node.d.ts.map +1 -0
- package/dist/components/native-node.js +137 -0
- package/dist/components/native-node.js.map +1 -0
- package/dist/components/types.d.ts +252 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +5 -0
- package/dist/components/types.js.map +1 -0
- package/dist/index-node.d.ts +10 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index-node.js +12 -0
- package/dist/index-node.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/index-node.ts +6 -6
- package/index.ts +4 -3
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +15 -8
- package/types/index.ts +0 -68
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { nativeLib as lib, ZPipelineRequest, ZPipelineResponse, ZPipelineResult, } from "./native-node.js";
|
|
2
|
+
import { ResponseCache } from "./cache.js";
|
|
3
|
+
import { MetricsCollector } from "./metrics.js";
|
|
4
|
+
import koffi from "koffi";
|
|
5
|
+
import zlib from "zlib";
|
|
6
|
+
const STATUS_TEXT = {
|
|
7
|
+
200: "OK",
|
|
8
|
+
201: "Created",
|
|
9
|
+
204: "No Content",
|
|
10
|
+
301: "Moved Permanently",
|
|
11
|
+
302: "Found",
|
|
12
|
+
400: "Bad Request",
|
|
13
|
+
401: "Unauthorized",
|
|
14
|
+
403: "Forbidden",
|
|
15
|
+
404: "Not Found",
|
|
16
|
+
500: "Internal Server Error",
|
|
17
|
+
502: "Bad Gateway",
|
|
18
|
+
503: "Service Unavailable",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Helper to define target URLs with type inference.
|
|
22
|
+
*/
|
|
23
|
+
export function defineUrls(urls) {
|
|
24
|
+
return urls;
|
|
25
|
+
}
|
|
26
|
+
// Cleanup native resources on GC
|
|
27
|
+
const clientRegistry = new FinalizationRegistry((ptr) => {
|
|
28
|
+
try {
|
|
29
|
+
lib.symbols.zclient_free(ptr);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Ignore cleanup errors
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export class JirenClient {
|
|
36
|
+
ptr = null; // Koffi pointer
|
|
37
|
+
urlMap = new Map();
|
|
38
|
+
cacheConfig = new Map();
|
|
39
|
+
antibotConfig = new Map();
|
|
40
|
+
cache;
|
|
41
|
+
inflightRequests = new Map();
|
|
42
|
+
globalRetry;
|
|
43
|
+
requestInterceptors = [];
|
|
44
|
+
responseInterceptors = [];
|
|
45
|
+
errorInterceptors = [];
|
|
46
|
+
targetsPromise = null;
|
|
47
|
+
targetsComplete = new Set();
|
|
48
|
+
performanceMode = false;
|
|
49
|
+
useDefaultHeaders = true;
|
|
50
|
+
// Pre-computed headers
|
|
51
|
+
defaultHeadersStr;
|
|
52
|
+
defaultHeaders = {
|
|
53
|
+
"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",
|
|
54
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
55
|
+
"accept-encoding": "gzip",
|
|
56
|
+
"accept-language": "en-US,en;q=0.9",
|
|
57
|
+
"sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
58
|
+
"sec-ch-ua-mobile": "?0",
|
|
59
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
60
|
+
"sec-fetch-dest": "document",
|
|
61
|
+
"sec-fetch-mode": "navigate",
|
|
62
|
+
"sec-fetch-site": "none",
|
|
63
|
+
"sec-fetch-user": "?1",
|
|
64
|
+
"upgrade-insecure-requests": "1",
|
|
65
|
+
};
|
|
66
|
+
/** Type-safe URL accessor for warmed-up URLs */
|
|
67
|
+
url;
|
|
68
|
+
// Metrics collector
|
|
69
|
+
metricsCollector;
|
|
70
|
+
/** Public metrics API */
|
|
71
|
+
metrics;
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.ptr = lib.symbols.zclient_new();
|
|
74
|
+
if (!this.ptr)
|
|
75
|
+
throw new Error("Failed to create native client instance");
|
|
76
|
+
clientRegistry.register(this, this.ptr, this);
|
|
77
|
+
// Pre-computed default headers string
|
|
78
|
+
const orderedKeys = [
|
|
79
|
+
"sec-ch-ua",
|
|
80
|
+
"sec-ch-ua-mobile",
|
|
81
|
+
"sec-ch-ua-platform",
|
|
82
|
+
"upgrade-insecure-requests",
|
|
83
|
+
"user-agent",
|
|
84
|
+
"accept",
|
|
85
|
+
"sec-fetch-site",
|
|
86
|
+
"sec-fetch-mode",
|
|
87
|
+
"sec-fetch-user",
|
|
88
|
+
"sec-fetch-dest",
|
|
89
|
+
"accept-encoding",
|
|
90
|
+
"accept-language",
|
|
91
|
+
];
|
|
92
|
+
this.defaultHeadersStr = orderedKeys
|
|
93
|
+
.map((k) => `${k}: ${this.defaultHeaders[k]}`)
|
|
94
|
+
.join("\r\n");
|
|
95
|
+
// Initialize cache
|
|
96
|
+
this.cache = new ResponseCache(100);
|
|
97
|
+
// Initialize metrics
|
|
98
|
+
this.metricsCollector = new MetricsCollector();
|
|
99
|
+
this.metrics = this.metricsCollector;
|
|
100
|
+
// Performance mode (default: true for maximum speed)
|
|
101
|
+
this.performanceMode = options?.performanceMode ?? true;
|
|
102
|
+
// Default headers (default: true)
|
|
103
|
+
this.useDefaultHeaders = options?.defaultHeaders ?? true;
|
|
104
|
+
// Enable benchmark mode if requested
|
|
105
|
+
if (options?.benchmark) {
|
|
106
|
+
lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
|
|
107
|
+
}
|
|
108
|
+
// Process target URLs
|
|
109
|
+
if (options?.targets) {
|
|
110
|
+
const urls = [];
|
|
111
|
+
const targets = options.targets;
|
|
112
|
+
if (Array.isArray(targets)) {
|
|
113
|
+
for (const item of targets) {
|
|
114
|
+
if (typeof item === "string") {
|
|
115
|
+
urls.push(item);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const config = item;
|
|
119
|
+
urls.push(config.url);
|
|
120
|
+
this.urlMap.set(config.key, config.url);
|
|
121
|
+
if (config.cache) {
|
|
122
|
+
const cacheConfig = typeof config.cache === "boolean"
|
|
123
|
+
? { enabled: true, ttl: 60000 }
|
|
124
|
+
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
125
|
+
this.cacheConfig.set(config.key, cacheConfig);
|
|
126
|
+
}
|
|
127
|
+
if (config.antibot) {
|
|
128
|
+
this.antibotConfig.set(config.key, true);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
for (const [key, urlConfig] of Object.entries(targets)) {
|
|
135
|
+
if (typeof urlConfig === "string") {
|
|
136
|
+
urls.push(urlConfig);
|
|
137
|
+
this.urlMap.set(key, urlConfig);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
urls.push(urlConfig.url);
|
|
141
|
+
this.urlMap.set(key, urlConfig.url);
|
|
142
|
+
if (urlConfig.cache) {
|
|
143
|
+
const cacheConfig = typeof urlConfig.cache === "boolean"
|
|
144
|
+
? { enabled: true, ttl: 60000 }
|
|
145
|
+
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
146
|
+
this.cacheConfig.set(key, cacheConfig);
|
|
147
|
+
}
|
|
148
|
+
if (urlConfig.antibot) {
|
|
149
|
+
this.antibotConfig.set(key, true);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (urls.length > 0) {
|
|
155
|
+
this.targetsPromise = this.preconnect(urls).then(() => {
|
|
156
|
+
urls.forEach((url) => this.targetsComplete.add(url));
|
|
157
|
+
this.targetsPromise = null;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// Preload L2 disk cache entries into L1 memory
|
|
161
|
+
for (const [key, config] of this.cacheConfig.entries()) {
|
|
162
|
+
if (config.enabled) {
|
|
163
|
+
const url = this.urlMap.get(key);
|
|
164
|
+
if (url) {
|
|
165
|
+
this.cache.preloadL1(url);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Create proxy for type-safe URL access
|
|
171
|
+
this.url = this.createUrlAccessor();
|
|
172
|
+
// Store global retry config
|
|
173
|
+
if (options?.retry) {
|
|
174
|
+
this.globalRetry =
|
|
175
|
+
typeof options.retry === "number"
|
|
176
|
+
? { count: options.retry, delay: 100, backoff: 2 }
|
|
177
|
+
: options.retry;
|
|
178
|
+
}
|
|
179
|
+
// Initialize interceptors
|
|
180
|
+
if (options?.interceptors) {
|
|
181
|
+
this.requestInterceptors = options.interceptors.request || [];
|
|
182
|
+
this.responseInterceptors = options.interceptors.response || [];
|
|
183
|
+
this.errorInterceptors = options.interceptors.error || [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async waitFor(ms) {
|
|
187
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Wait for lazy pre-connection to complete.
|
|
191
|
+
*/
|
|
192
|
+
async waitForTargets() {
|
|
193
|
+
if (this.targetsPromise)
|
|
194
|
+
await this.targetsPromise;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated Use waitForTargets() instead
|
|
198
|
+
*/
|
|
199
|
+
async waitForWarmup() {
|
|
200
|
+
return this.waitForTargets();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Creates a proxy-based URL accessor for type-safe access.
|
|
204
|
+
*/
|
|
205
|
+
createUrlAccessor() {
|
|
206
|
+
const self = this;
|
|
207
|
+
return new Proxy({}, {
|
|
208
|
+
get(_target, prop) {
|
|
209
|
+
const baseUrl = self.urlMap.get(prop);
|
|
210
|
+
if (!baseUrl) {
|
|
211
|
+
throw new Error(`URL key "${prop}" not found. Available keys: ${Array.from(self.urlMap.keys()).join(", ")}`);
|
|
212
|
+
}
|
|
213
|
+
const buildUrl = (path) => path
|
|
214
|
+
? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
|
|
215
|
+
: baseUrl;
|
|
216
|
+
return {
|
|
217
|
+
get: async (options) => {
|
|
218
|
+
if (self.targetsPromise) {
|
|
219
|
+
await self.targetsPromise;
|
|
220
|
+
}
|
|
221
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
222
|
+
const useAntibot = options?.antibot ?? self.antibotConfig.get(prop) ?? false;
|
|
223
|
+
// Fast path: no cache
|
|
224
|
+
if (!cacheConfig?.enabled && self.performanceMode) {
|
|
225
|
+
return self.request("GET", buildUrl(options?.path), null, {
|
|
226
|
+
headers: options?.headers,
|
|
227
|
+
maxRedirects: options?.maxRedirects,
|
|
228
|
+
responseType: options?.responseType,
|
|
229
|
+
antibot: useAntibot,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const startTime = performance.now();
|
|
233
|
+
// Try cache
|
|
234
|
+
if (cacheConfig?.enabled) {
|
|
235
|
+
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
236
|
+
if (cached) {
|
|
237
|
+
const responseTimeMs = performance.now() - startTime;
|
|
238
|
+
const cacheLayer = responseTimeMs < 0.5 ? "l1" : "l2";
|
|
239
|
+
if (!self.performanceMode) {
|
|
240
|
+
self.metricsCollector.recordRequest(prop, {
|
|
241
|
+
startTime,
|
|
242
|
+
responseTimeMs,
|
|
243
|
+
status: cached.status,
|
|
244
|
+
success: cached.ok,
|
|
245
|
+
bytesSent: 0,
|
|
246
|
+
bytesReceived: 0,
|
|
247
|
+
cacheHit: true,
|
|
248
|
+
cacheLayer,
|
|
249
|
+
dedupeHit: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return cached;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Deduplication
|
|
256
|
+
const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(options?.headers || {})}`;
|
|
257
|
+
if (self.inflightRequests.has(dedupKey)) {
|
|
258
|
+
const dedupeStart = performance.now();
|
|
259
|
+
const result = await self.inflightRequests.get(dedupKey);
|
|
260
|
+
const responseTimeMs = performance.now() - dedupeStart;
|
|
261
|
+
if (!self.performanceMode) {
|
|
262
|
+
self.metricsCollector.recordRequest(prop, {
|
|
263
|
+
startTime: dedupeStart,
|
|
264
|
+
responseTimeMs,
|
|
265
|
+
status: typeof result === "object" && "status" in result
|
|
266
|
+
? result.status
|
|
267
|
+
: 200,
|
|
268
|
+
success: true,
|
|
269
|
+
bytesSent: 0,
|
|
270
|
+
bytesReceived: 0,
|
|
271
|
+
cacheHit: false,
|
|
272
|
+
dedupeHit: true,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
const requestPromise = (async () => {
|
|
278
|
+
try {
|
|
279
|
+
const response = await self.request("GET", buildUrl(options?.path), null, {
|
|
280
|
+
headers: options?.headers,
|
|
281
|
+
maxRedirects: options?.maxRedirects,
|
|
282
|
+
responseType: options?.responseType,
|
|
283
|
+
antibot: useAntibot,
|
|
284
|
+
});
|
|
285
|
+
if (cacheConfig?.enabled &&
|
|
286
|
+
typeof response === "object" &&
|
|
287
|
+
"status" in response) {
|
|
288
|
+
self.cache.set(baseUrl, response, cacheConfig.ttl, options?.path, options);
|
|
289
|
+
}
|
|
290
|
+
const responseTimeMs = performance.now() - startTime;
|
|
291
|
+
if (!self.performanceMode) {
|
|
292
|
+
self.metricsCollector.recordRequest(prop, {
|
|
293
|
+
startTime,
|
|
294
|
+
responseTimeMs,
|
|
295
|
+
status: typeof response === "object" && "status" in response
|
|
296
|
+
? response.status
|
|
297
|
+
: 200,
|
|
298
|
+
success: typeof response === "object" && "ok" in response
|
|
299
|
+
? response.ok
|
|
300
|
+
: true,
|
|
301
|
+
bytesSent: options?.body
|
|
302
|
+
? JSON.stringify(options.body).length
|
|
303
|
+
: 0,
|
|
304
|
+
bytesReceived: 0,
|
|
305
|
+
cacheHit: false,
|
|
306
|
+
dedupeHit: false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return response;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (!self.performanceMode) {
|
|
313
|
+
const responseTimeMs = performance.now() - startTime;
|
|
314
|
+
self.metricsCollector.recordRequest(prop, {
|
|
315
|
+
startTime,
|
|
316
|
+
responseTimeMs,
|
|
317
|
+
status: 0,
|
|
318
|
+
success: false,
|
|
319
|
+
bytesSent: 0,
|
|
320
|
+
bytesReceived: 0,
|
|
321
|
+
cacheHit: false,
|
|
322
|
+
dedupeHit: false,
|
|
323
|
+
error: error instanceof Error ? error.message : String(error),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
self.inflightRequests.delete(dedupKey);
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
self.inflightRequests.set(dedupKey, requestPromise);
|
|
333
|
+
return requestPromise;
|
|
334
|
+
},
|
|
335
|
+
post: async (options) => {
|
|
336
|
+
const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
|
|
337
|
+
return self.request("POST", buildUrl(options?.path), serializedBody, {
|
|
338
|
+
headers,
|
|
339
|
+
maxRedirects: options?.maxRedirects,
|
|
340
|
+
responseType: options?.responseType,
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
put: async (options) => {
|
|
344
|
+
const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
|
|
345
|
+
return self.request("PUT", buildUrl(options?.path), serializedBody, {
|
|
346
|
+
headers,
|
|
347
|
+
maxRedirects: options?.maxRedirects,
|
|
348
|
+
responseType: options?.responseType,
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
patch: async (options) => {
|
|
352
|
+
const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
|
|
353
|
+
return self.request("PATCH", buildUrl(options?.path), serializedBody, {
|
|
354
|
+
headers,
|
|
355
|
+
maxRedirects: options?.maxRedirects,
|
|
356
|
+
responseType: options?.responseType,
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
delete: async (options) => {
|
|
360
|
+
const { headers, serializedBody } = self.prepareBody(options?.body, options?.headers);
|
|
361
|
+
return self.request("DELETE", buildUrl(options?.path), serializedBody, {
|
|
362
|
+
headers,
|
|
363
|
+
maxRedirects: options?.maxRedirects,
|
|
364
|
+
responseType: options?.responseType,
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
head: async (options) => {
|
|
368
|
+
return self.request("HEAD", buildUrl(options?.path), null, {
|
|
369
|
+
headers: options?.headers,
|
|
370
|
+
maxRedirects: options?.maxRedirects,
|
|
371
|
+
antibot: options?.antibot,
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
options: async (options) => {
|
|
375
|
+
return self.request("OPTIONS", buildUrl(options?.path), null, {
|
|
376
|
+
headers: options?.headers,
|
|
377
|
+
maxRedirects: options?.maxRedirects,
|
|
378
|
+
antibot: options?.antibot,
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
prefetch: async (options) => {
|
|
382
|
+
self.cache.clear(baseUrl);
|
|
383
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
384
|
+
if (cacheConfig?.enabled) {
|
|
385
|
+
await self.request("GET", buildUrl(options?.path), null, {
|
|
386
|
+
headers: options?.headers,
|
|
387
|
+
maxRedirects: options?.maxRedirects,
|
|
388
|
+
antibot: options?.antibot,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Free the native client resources.
|
|
398
|
+
* Note: This is called automatically when the client is garbage collected,
|
|
399
|
+
* or you can use the `using` keyword for automatic cleanup in a scope.
|
|
400
|
+
*/
|
|
401
|
+
close() {
|
|
402
|
+
if (this.ptr) {
|
|
403
|
+
// Unregister from FinalizationRegistry since we're manually closing
|
|
404
|
+
clientRegistry.unregister(this);
|
|
405
|
+
lib.symbols.zclient_free(this.ptr);
|
|
406
|
+
this.ptr = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Dispose method for the `using` keyword (ECMAScript Explicit Resource Management)
|
|
411
|
+
* @example
|
|
412
|
+
* ```typescript
|
|
413
|
+
* using client = new JirenClient({ targets: [...] });
|
|
414
|
+
* // client is automatically closed when the scope ends
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
[Symbol.dispose]() {
|
|
418
|
+
this.close();
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Register interceptors dynamically.
|
|
422
|
+
*/
|
|
423
|
+
use(interceptors) {
|
|
424
|
+
if (interceptors.request)
|
|
425
|
+
this.requestInterceptors.push(...interceptors.request);
|
|
426
|
+
if (interceptors.response)
|
|
427
|
+
this.responseInterceptors.push(...interceptors.response);
|
|
428
|
+
if (interceptors.error)
|
|
429
|
+
this.errorInterceptors.push(...interceptors.error);
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Pre-connect to URLs in parallel.
|
|
434
|
+
*/
|
|
435
|
+
async preconnect(urls) {
|
|
436
|
+
if (!this.ptr)
|
|
437
|
+
throw new Error("Client is closed");
|
|
438
|
+
await Promise.all(urls.map((url) => new Promise((resolve) => {
|
|
439
|
+
lib.symbols.zclient_prefetch(this.ptr, url);
|
|
440
|
+
resolve();
|
|
441
|
+
})));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* @deprecated Use preconnect() instead
|
|
445
|
+
*/
|
|
446
|
+
async warmup(urls) {
|
|
447
|
+
return this.preconnect(urls);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* @deprecated Use preconnect() instead
|
|
451
|
+
*/
|
|
452
|
+
prefetch(urls) {
|
|
453
|
+
this.preconnect(urls);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Pre-warm a single URL (establishes connection, caches DNS, TLS session)
|
|
457
|
+
* Call this before making requests to eliminate first-request latency.
|
|
458
|
+
*/
|
|
459
|
+
async prewarm(url) {
|
|
460
|
+
return this.preconnect([url]);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Execute multiple requests in a single pipelined batch (100k+ RPS)
|
|
464
|
+
* All requests must go to the same host.
|
|
465
|
+
* @param host - The host (e.g., "localhost")
|
|
466
|
+
* @param port - The port (e.g., 4000)
|
|
467
|
+
* @param requests - Array of { method, path } objects
|
|
468
|
+
* @returns Array of response bodies as strings
|
|
469
|
+
*/
|
|
470
|
+
batch(host, port, requests) {
|
|
471
|
+
if (!this.ptr)
|
|
472
|
+
throw new Error("Client is closed");
|
|
473
|
+
if (requests.length === 0)
|
|
474
|
+
return [];
|
|
475
|
+
// Encode strings to buffers and create request structs
|
|
476
|
+
const requestStructs = [];
|
|
477
|
+
const buffers = []; // Keep references to prevent GC
|
|
478
|
+
for (const req of requests) {
|
|
479
|
+
const methodBuf = Buffer.from(req.method + "\0");
|
|
480
|
+
const pathBuf = Buffer.from(req.path + "\0");
|
|
481
|
+
buffers.push(methodBuf, pathBuf);
|
|
482
|
+
requestStructs.push({
|
|
483
|
+
method_ptr: methodBuf,
|
|
484
|
+
method_len: req.method.length,
|
|
485
|
+
path_ptr: pathBuf,
|
|
486
|
+
path_len: req.path.length,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
// Create array of structs
|
|
490
|
+
const requestArray = koffi.as(requestStructs, koffi.pointer(ZPipelineRequest));
|
|
491
|
+
// Call native pipelined batch
|
|
492
|
+
const resultPtr = lib.symbols.zclient_request_batch_http1(this.ptr, host, port, requestArray, requests.length);
|
|
493
|
+
if (!resultPtr) {
|
|
494
|
+
throw new Error("Batch request failed");
|
|
495
|
+
}
|
|
496
|
+
// Read result struct
|
|
497
|
+
const result = koffi.decode(resultPtr, ZPipelineResult);
|
|
498
|
+
const count = Number(result.count);
|
|
499
|
+
// Read response array
|
|
500
|
+
const responses = [];
|
|
501
|
+
const responseArray = koffi.decode(result.responses, koffi.array(ZPipelineResponse, count));
|
|
502
|
+
for (let i = 0; i < count; i++) {
|
|
503
|
+
const resp = responseArray[i];
|
|
504
|
+
const bodyLen = Number(resp.body_len);
|
|
505
|
+
if (bodyLen > 0) {
|
|
506
|
+
const bodyBuf = koffi.decode(resp.body_ptr, koffi.array("uint8_t", bodyLen));
|
|
507
|
+
responses.push(Buffer.from(bodyBuf).toString("utf-8"));
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
responses.push("");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Free native memory
|
|
514
|
+
lib.symbols.zclient_pipeline_result_free(resultPtr);
|
|
515
|
+
return responses;
|
|
516
|
+
}
|
|
517
|
+
async request(method, url, body, options) {
|
|
518
|
+
if (!this.ptr)
|
|
519
|
+
throw new Error("Client is closed");
|
|
520
|
+
// Normalize options
|
|
521
|
+
let headers = {};
|
|
522
|
+
let maxRedirects = 5;
|
|
523
|
+
let responseType;
|
|
524
|
+
let antibot = false;
|
|
525
|
+
if (options) {
|
|
526
|
+
if ("maxRedirects" in options ||
|
|
527
|
+
"headers" in options ||
|
|
528
|
+
"responseType" in options ||
|
|
529
|
+
"method" in options ||
|
|
530
|
+
"timeout" in options ||
|
|
531
|
+
"antibot" in options) {
|
|
532
|
+
const opts = options;
|
|
533
|
+
if (opts.headers)
|
|
534
|
+
headers = opts.headers;
|
|
535
|
+
if (opts.maxRedirects !== undefined)
|
|
536
|
+
maxRedirects = opts.maxRedirects;
|
|
537
|
+
if (opts.responseType)
|
|
538
|
+
responseType = opts.responseType;
|
|
539
|
+
if (opts.antibot !== undefined)
|
|
540
|
+
antibot = opts.antibot;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
headers = options;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Run interceptors
|
|
547
|
+
let ctx = { method, url, headers, body };
|
|
548
|
+
if (this.requestInterceptors.length > 0) {
|
|
549
|
+
for (const interceptor of this.requestInterceptors) {
|
|
550
|
+
ctx = await interceptor(ctx);
|
|
551
|
+
}
|
|
552
|
+
method = ctx.method;
|
|
553
|
+
url = ctx.url;
|
|
554
|
+
headers = ctx.headers;
|
|
555
|
+
body = ctx.body ?? null;
|
|
556
|
+
}
|
|
557
|
+
// Prepare headers
|
|
558
|
+
let headerStr;
|
|
559
|
+
const hasCustomHeaders = Object.keys(headers).length > 0;
|
|
560
|
+
if (hasCustomHeaders) {
|
|
561
|
+
const finalHeaders = this.useDefaultHeaders
|
|
562
|
+
? { ...this.defaultHeaders, ...headers }
|
|
563
|
+
: headers;
|
|
564
|
+
const orderedHeaders = {};
|
|
565
|
+
const keys = [
|
|
566
|
+
"sec-ch-ua",
|
|
567
|
+
"sec-ch-ua-mobile",
|
|
568
|
+
"sec-ch-ua-platform",
|
|
569
|
+
"upgrade-insecure-requests",
|
|
570
|
+
"user-agent",
|
|
571
|
+
"accept",
|
|
572
|
+
"sec-fetch-site",
|
|
573
|
+
"sec-fetch-mode",
|
|
574
|
+
"sec-fetch-user",
|
|
575
|
+
"sec-fetch-dest",
|
|
576
|
+
"accept-encoding",
|
|
577
|
+
"accept-language",
|
|
578
|
+
];
|
|
579
|
+
for (const key of keys) {
|
|
580
|
+
if (finalHeaders[key]) {
|
|
581
|
+
orderedHeaders[key] = finalHeaders[key];
|
|
582
|
+
delete finalHeaders[key];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
586
|
+
orderedHeaders[key] = value;
|
|
587
|
+
}
|
|
588
|
+
headerStr = Object.entries(orderedHeaders)
|
|
589
|
+
.map(([k, v]) => `${k.toLowerCase()}: ${v}`)
|
|
590
|
+
.join("\r\n");
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
headerStr = this.useDefaultHeaders ? this.defaultHeadersStr : "";
|
|
594
|
+
}
|
|
595
|
+
// Retry logic
|
|
596
|
+
let retryConfig = this.globalRetry;
|
|
597
|
+
if (options && typeof options === "object" && "retry" in options) {
|
|
598
|
+
const userRetry = options.retry;
|
|
599
|
+
if (typeof userRetry === "number") {
|
|
600
|
+
retryConfig = { count: userRetry, delay: 100, backoff: 2 };
|
|
601
|
+
}
|
|
602
|
+
else if (typeof userRetry === "object") {
|
|
603
|
+
retryConfig = userRetry;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
let attempts = 0;
|
|
607
|
+
const maxAttempts = (retryConfig?.count || 0) + 1;
|
|
608
|
+
let currentDelay = retryConfig?.delay || 100;
|
|
609
|
+
const backoff = retryConfig?.backoff || 2;
|
|
610
|
+
let lastError;
|
|
611
|
+
while (attempts < maxAttempts) {
|
|
612
|
+
attempts++;
|
|
613
|
+
try {
|
|
614
|
+
const respPtr = lib.symbols.zclient_request(this.ptr, method, url, headerStr.length > 0 ? headerStr : null, body || null, maxRedirects, antibot);
|
|
615
|
+
if (!respPtr) {
|
|
616
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
617
|
+
}
|
|
618
|
+
const response = this.parseResponse(respPtr, url);
|
|
619
|
+
// Run response interceptors
|
|
620
|
+
let finalResponse = response;
|
|
621
|
+
if (this.responseInterceptors.length > 0) {
|
|
622
|
+
let responseCtx = {
|
|
623
|
+
request: ctx,
|
|
624
|
+
response,
|
|
625
|
+
};
|
|
626
|
+
for (const interceptor of this.responseInterceptors) {
|
|
627
|
+
responseCtx = await interceptor(responseCtx);
|
|
628
|
+
}
|
|
629
|
+
finalResponse = responseCtx.response;
|
|
630
|
+
}
|
|
631
|
+
if (responseType) {
|
|
632
|
+
if (responseType === "json")
|
|
633
|
+
return finalResponse.body.json();
|
|
634
|
+
if (responseType === "text")
|
|
635
|
+
return finalResponse.body.text();
|
|
636
|
+
if (responseType === "arraybuffer")
|
|
637
|
+
return finalResponse.body.arrayBuffer();
|
|
638
|
+
if (responseType === "blob")
|
|
639
|
+
return finalResponse.body.blob();
|
|
640
|
+
}
|
|
641
|
+
return finalResponse;
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
if (this.errorInterceptors.length > 0) {
|
|
645
|
+
for (const interceptor of this.errorInterceptors) {
|
|
646
|
+
await interceptor(err, ctx);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
lastError = err;
|
|
650
|
+
if (attempts < maxAttempts) {
|
|
651
|
+
await this.waitFor(currentDelay);
|
|
652
|
+
currentDelay *= backoff;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
throw lastError || new Error("Request failed after retries");
|
|
657
|
+
}
|
|
658
|
+
parseResponse(respPtr, url) {
|
|
659
|
+
if (!respPtr)
|
|
660
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
661
|
+
try {
|
|
662
|
+
const status = lib.symbols.zclient_response_status(respPtr);
|
|
663
|
+
const len = Number(lib.symbols.zclient_response_body_len(respPtr));
|
|
664
|
+
const bodyPtr = lib.symbols.zclient_response_body(respPtr);
|
|
665
|
+
const headersLen = Number(lib.symbols.zclient_response_headers_len(respPtr));
|
|
666
|
+
let headersObj = {};
|
|
667
|
+
if (headersLen > 0) {
|
|
668
|
+
const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
|
|
669
|
+
if (rawHeadersPtr) {
|
|
670
|
+
const raw = Buffer.from(koffi.decode(rawHeadersPtr, "uint8_t", headersLen));
|
|
671
|
+
headersObj = new NativeHeaders(raw);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const headersProxy = new Proxy(headersObj instanceof NativeHeaders ? headersObj : {}, {
|
|
675
|
+
get(target, prop) {
|
|
676
|
+
if (target instanceof NativeHeaders && typeof prop === "string") {
|
|
677
|
+
if (prop === "toJSON")
|
|
678
|
+
return () => target.toJSON();
|
|
679
|
+
const val = target.get(prop);
|
|
680
|
+
if (val !== null)
|
|
681
|
+
return val;
|
|
682
|
+
}
|
|
683
|
+
return Reflect.get(target, prop);
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
let buffer = Buffer.alloc(0);
|
|
687
|
+
if (len > 0 && bodyPtr) {
|
|
688
|
+
buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
|
|
689
|
+
// Handle GZIP
|
|
690
|
+
const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
|
|
691
|
+
let gzipOffset = -1;
|
|
692
|
+
for (let i = 0; i < Math.min(16, buffer.length - 1); i++) {
|
|
693
|
+
if (buffer[i] === 0x1f && buffer[i + 1] === 0x8b) {
|
|
694
|
+
gzipOffset = i;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (contentEncoding === "gzip" || gzipOffset >= 0) {
|
|
699
|
+
try {
|
|
700
|
+
const gzipData = gzipOffset > 0 ? buffer.slice(gzipOffset) : buffer;
|
|
701
|
+
buffer = zlib.gunzipSync(gzipData);
|
|
702
|
+
}
|
|
703
|
+
catch (e) {
|
|
704
|
+
// Keep original buffer
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
let bodyUsed = false;
|
|
709
|
+
const consumeBody = () => {
|
|
710
|
+
bodyUsed = true;
|
|
711
|
+
};
|
|
712
|
+
const bodyObj = {
|
|
713
|
+
bodyUsed: false,
|
|
714
|
+
arrayBuffer: async () => {
|
|
715
|
+
consumeBody();
|
|
716
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
717
|
+
},
|
|
718
|
+
blob: async () => {
|
|
719
|
+
consumeBody();
|
|
720
|
+
return new Blob([buffer]);
|
|
721
|
+
},
|
|
722
|
+
text: async () => {
|
|
723
|
+
consumeBody();
|
|
724
|
+
return buffer.toString("utf-8");
|
|
725
|
+
},
|
|
726
|
+
json: async () => {
|
|
727
|
+
consumeBody();
|
|
728
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
732
|
+
get: () => bodyUsed,
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
url,
|
|
736
|
+
status,
|
|
737
|
+
statusText: STATUS_TEXT[status] || "",
|
|
738
|
+
headers: headersProxy,
|
|
739
|
+
ok: status >= 200 && status < 300,
|
|
740
|
+
redirected: false,
|
|
741
|
+
type: "basic",
|
|
742
|
+
body: bodyObj,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
lib.symbols.zclient_response_free(respPtr);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Helper to prepare body and headers for requests.
|
|
751
|
+
*/
|
|
752
|
+
prepareBody(body, userHeaders) {
|
|
753
|
+
let serializedBody = null;
|
|
754
|
+
const headers = { ...userHeaders };
|
|
755
|
+
if (body !== null && body !== undefined) {
|
|
756
|
+
if (typeof body === "object") {
|
|
757
|
+
serializedBody = JSON.stringify(body);
|
|
758
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
759
|
+
if (!hasContentType) {
|
|
760
|
+
headers["Content-Type"] = "application/json";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
serializedBody = String(body);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return { headers, serializedBody };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
class NativeHeaders {
|
|
771
|
+
raw;
|
|
772
|
+
len;
|
|
773
|
+
cache = new Map();
|
|
774
|
+
parsed = false;
|
|
775
|
+
constructor(raw) {
|
|
776
|
+
this.raw = raw;
|
|
777
|
+
this.len = raw.length;
|
|
778
|
+
}
|
|
779
|
+
ensureParsed() {
|
|
780
|
+
if (this.parsed)
|
|
781
|
+
return;
|
|
782
|
+
try {
|
|
783
|
+
const text = this.raw.toString("utf-8");
|
|
784
|
+
const lines = text.split("\r\n");
|
|
785
|
+
for (const line of lines) {
|
|
786
|
+
if (!line)
|
|
787
|
+
continue;
|
|
788
|
+
const colon = line.indexOf(":");
|
|
789
|
+
if (colon === -1)
|
|
790
|
+
continue;
|
|
791
|
+
const key = line.substring(0, colon).trim().toLowerCase();
|
|
792
|
+
const val = line.substring(colon + 1).trim();
|
|
793
|
+
this.cache.set(key, val);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (e) {
|
|
797
|
+
// Ignore parsing errors
|
|
798
|
+
}
|
|
799
|
+
this.parsed = true;
|
|
800
|
+
}
|
|
801
|
+
get(name) {
|
|
802
|
+
const target = name.toLowerCase();
|
|
803
|
+
this.ensureParsed();
|
|
804
|
+
return this.cache.get(target) || null;
|
|
805
|
+
}
|
|
806
|
+
toJSON() {
|
|
807
|
+
this.ensureParsed();
|
|
808
|
+
return Object.fromEntries(this.cache);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
//# sourceMappingURL=client-node-native.js.map
|