jiren 1.5.0 → 1.6.0

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