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.
Files changed (45) hide show
  1. package/README.md +123 -313
  2. package/components/cache.ts +1 -1
  3. package/components/client-node-native.ts +602 -159
  4. package/components/client.ts +51 -29
  5. package/components/metrics.ts +1 -4
  6. package/components/native-node.ts +48 -3
  7. package/components/native.ts +29 -0
  8. package/components/persistent-worker.ts +73 -0
  9. package/components/subprocess-worker.ts +65 -0
  10. package/components/types.ts +2 -0
  11. package/components/worker-pool.ts +169 -0
  12. package/components/worker.ts +39 -23
  13. package/dist/components/cache.d.ts +76 -0
  14. package/dist/components/cache.d.ts.map +1 -0
  15. package/dist/components/cache.js +439 -0
  16. package/dist/components/cache.js.map +1 -0
  17. package/dist/components/client-node-native.d.ts +134 -0
  18. package/dist/components/client-node-native.d.ts.map +1 -0
  19. package/dist/components/client-node-native.js +811 -0
  20. package/dist/components/client-node-native.js.map +1 -0
  21. package/dist/components/metrics.d.ts +104 -0
  22. package/dist/components/metrics.d.ts.map +1 -0
  23. package/dist/components/metrics.js +296 -0
  24. package/dist/components/metrics.js.map +1 -0
  25. package/dist/components/native-node.d.ts +67 -0
  26. package/dist/components/native-node.d.ts.map +1 -0
  27. package/dist/components/native-node.js +137 -0
  28. package/dist/components/native-node.js.map +1 -0
  29. package/dist/components/types.d.ts +252 -0
  30. package/dist/components/types.d.ts.map +1 -0
  31. package/dist/components/types.js +5 -0
  32. package/dist/components/types.js.map +1 -0
  33. package/dist/index-node.d.ts +10 -0
  34. package/dist/index-node.d.ts.map +1 -0
  35. package/dist/index-node.js +12 -0
  36. package/dist/index-node.js.map +1 -0
  37. package/dist/types/index.d.ts +63 -0
  38. package/dist/types/index.d.ts.map +1 -0
  39. package/dist/types/index.js +6 -0
  40. package/dist/types/index.js.map +1 -0
  41. package/index-node.ts +6 -6
  42. package/index.ts +4 -3
  43. package/lib/libhttpclient.dylib +0 -0
  44. package/package.json +15 -8
  45. 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