jiren 3.1.0 → 3.3.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 (34) hide show
  1. package/README.md +1 -9
  2. package/components/client-node-native.ts +150 -292
  3. package/components/client.ts +385 -286
  4. package/components/index.ts +0 -2
  5. package/components/native-cache-node.ts +0 -3
  6. package/components/native-cache.ts +0 -8
  7. package/components/native-node.ts +142 -4
  8. package/components/native.ts +33 -65
  9. package/components/types.ts +25 -41
  10. package/dist/components/client-node-native.d.ts +0 -1
  11. package/dist/components/client-node-native.d.ts.map +1 -1
  12. package/dist/components/client-node-native.js +84 -202
  13. package/dist/components/client-node-native.js.map +1 -1
  14. package/dist/components/native-cache-node.d.ts.map +1 -1
  15. package/dist/components/native-cache-node.js +0 -1
  16. package/dist/components/native-cache-node.js.map +1 -1
  17. package/dist/components/native-node.d.ts +78 -0
  18. package/dist/components/native-node.d.ts.map +1 -1
  19. package/dist/components/native-node.js +125 -2
  20. package/dist/components/native-node.js.map +1 -1
  21. package/dist/components/types.d.ts +5 -13
  22. package/dist/components/types.d.ts.map +1 -1
  23. package/lib/libhttpclient.dylib +0 -0
  24. package/package.json +3 -3
  25. package/components/cache.ts +0 -451
  26. package/components/native-json.ts +0 -195
  27. package/components/persistent-worker.ts +0 -67
  28. package/components/subprocess-worker.ts +0 -60
  29. package/components/worker-pool.ts +0 -153
  30. package/components/worker.ts +0 -154
  31. package/dist/components/cache.d.ts +0 -32
  32. package/dist/components/cache.d.ts.map +0 -1
  33. package/dist/components/cache.js +0 -374
  34. package/dist/components/cache.js.map +0 -1
@@ -1,23 +1,18 @@
1
- import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
1
+ import { toArrayBuffer, ptr, type Pointer } from "bun:ffi";
2
2
  import { lib } from "./native";
3
3
  import { NativeCache } from "./native-cache";
4
- import { parseJsonFields } from "./native-json";
4
+
5
5
  import { MetricsCollector } from "./metrics";
6
6
  import type {
7
- // New imports
8
7
  UrlConfig,
9
8
  JirenClientOptions,
10
- ExtractTargetKeys,
11
9
  UrlAccessor,
12
- RequestMetric,
13
- // Existing imports
14
10
  RequestOptions,
15
11
  JirenResponse,
16
12
  JirenResponseBody,
17
13
  TargetUrlConfig,
18
14
  UrlRequestOptions,
19
15
  UrlEndpoint,
20
- CacheConfig,
21
16
  RetryConfig,
22
17
  Interceptors,
23
18
  RequestInterceptor,
@@ -26,8 +21,6 @@ import type {
26
21
  InterceptorRequestContext,
27
22
  InterceptorResponseContext,
28
23
  MetricsAPI,
29
- // Progress tracking
30
- ProgressEvent,
31
24
  ProgressRequestOptions,
32
25
  } from "./types";
33
26
 
@@ -46,27 +39,22 @@ const STATUS_TEXT: Record<number, string> = {
46
39
  };
47
40
 
48
41
  export function defineUrls<const T extends readonly TargetUrlConfig[]>(
49
- urls: T
42
+ urls: T,
50
43
  ): T {
51
44
  return urls;
52
45
  }
53
46
 
54
- // FinalizationRegistry for automatic cleanup when client is garbage collected
55
47
  const clientRegistry = new FinalizationRegistry<number>((ptrValue) => {
56
- // Note: We store the pointer as a number since FinalizationRegistry can't hold Pointer directly
57
48
  try {
58
49
  lib.symbols.zclient_free(ptrValue as any);
59
- } catch {
60
- // Ignore errors during cleanup
61
- }
50
+ } catch {}
62
51
  });
63
52
 
64
53
  export class JirenClient<
65
54
  T extends readonly TargetUrlConfig[] | Record<string, UrlConfig> =
66
55
  | readonly TargetUrlConfig[]
67
- | Record<string, UrlConfig>
68
- > implements Disposable
69
- {
56
+ | Record<string, UrlConfig>,
57
+ > implements Disposable {
70
58
  private ptr: Pointer | null;
71
59
  private urlMap: Map<string, string> = new Map();
72
60
  private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
@@ -114,6 +102,33 @@ export class JirenClient<
114
102
  OPTIONS: Buffer.from("OPTIONS\0"),
115
103
  };
116
104
 
105
+ // URL buffer cache for warmed endpoints (avoids per-request Buffer.from allocation)
106
+ private readonly urlBufferCache: Map<string, Buffer> = new Map();
107
+ private static readonly URL_BUFFER_CACHE_MAX = 100;
108
+
109
+ // Endpoint cache - pre-created endpoint objects to avoid Proxy allocation overhead
110
+ private readonly endpointCache: Map<string, UrlEndpoint> = new Map();
111
+
112
+ // Cached FFI symbol references (eliminates property lookup overhead)
113
+ private readonly _requestFull = lib.symbols.zclient_request_full;
114
+ private readonly _responseFullFree = lib.symbols.zclient_response_full_free;
115
+ private readonly _fullResponseStatus = lib.symbols.zfull_response_status;
116
+ private readonly _fullResponseBody = lib.symbols.zfull_response_body;
117
+ private readonly _fullResponseBodyLen = lib.symbols.zfull_response_body_len;
118
+ private readonly _fullResponseHeaders = lib.symbols.zfull_response_headers;
119
+ private readonly _fullResponseHeadersLen =
120
+ lib.symbols.zfull_response_headers_len;
121
+ private readonly _fullResponseWasDecompressed =
122
+ lib.symbols.zfull_response_was_decompressed;
123
+ private readonly _simdKeyMatch = lib.symbols.z_simd_key_match;
124
+ private readonly _findDoubleCRLF = lib.symbols.z_find_double_crlf;
125
+ private readonly _getJsonFields = lib.symbols.zclient_get_json_fields;
126
+ private readonly _requestJsonFields = lib.symbols.zclient_request_json_fields;
127
+ private readonly _jsonFieldsFree = lib.symbols.zclient_json_fields_free;
128
+ private readonly _fieldStr = lib.symbols.zjson_field_str;
129
+ private readonly _fieldLen = lib.symbols.zjson_field_len;
130
+ private readonly _fieldType = lib.symbols.zjson_field_type;
131
+
117
132
  // Reusable TextDecoder
118
133
  private readonly decoder = new TextDecoder();
119
134
 
@@ -149,20 +164,15 @@ export class JirenClient<
149
164
  this.defaultHeadersStr = orderedKeys
150
165
  .map((k) => `${k}: ${this.defaultHeaders[k]}`)
151
166
  .join("\r\n");
152
- // Cache the Buffer to avoid per-request allocation
153
167
  this.defaultHeadersBuffer = Buffer.from(this.defaultHeadersStr + "\0");
154
168
 
155
- // Initialize native cache (faster than JS implementation)
156
169
  this.cache = new NativeCache(100);
157
170
 
158
- // Initialize metrics
159
171
  this.metricsCollector = new MetricsCollector();
160
172
  this.metrics = this.metricsCollector;
161
173
 
162
- // Performance mode (default: true for maximum speed, set false to enable metrics)
163
174
  this.performanceMode = options?.performanceMode ?? true;
164
175
 
165
- // Enable benchmark mode if requested
166
176
  if (options?.benchmark) {
167
177
  lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
168
178
  }
@@ -201,7 +211,7 @@ export class JirenClient<
201
211
  // Record<string, UrlConfig>
202
212
  for (const [key, urlConfig] of Object.entries(targets) as [
203
213
  string,
204
- UrlConfig
214
+ UrlConfig,
205
215
  ][]) {
206
216
  if (typeof urlConfig === "string") {
207
217
  // Simple string URL
@@ -288,12 +298,16 @@ export class JirenClient<
288
298
 
289
299
  return new Proxy({} as UrlAccessor<T>, {
290
300
  get(_target, prop: string) {
301
+ // Fast path: return cached endpoint
302
+ const cached = self.endpointCache.get(prop);
303
+ if (cached) return cached;
304
+
291
305
  const baseUrl = self.urlMap.get(prop);
292
306
  if (!baseUrl) {
293
307
  throw new Error(
294
308
  `URL key "${prop}" not found. Available keys: ${Array.from(
295
- self.urlMap.keys()
296
- ).join(", ")}`
309
+ self.urlMap.keys(),
310
+ ).join(", ")}`,
297
311
  );
298
312
  }
299
313
 
@@ -303,22 +317,48 @@ export class JirenClient<
303
317
  ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
304
318
  : baseUrl;
305
319
 
306
- // Return a UrlEndpoint object with all HTTP methods
307
- return {
320
+ // Pre-compute values at endpoint creation time (not per-request)
321
+ const cachedAntibot = self.antibotConfig.get(prop) ?? false;
322
+ const cacheConfig = self.cacheConfig.get(prop);
323
+ const urlBuffer = self.getUrlBuffer(baseUrl);
324
+
325
+ // Create and cache the endpoint object
326
+ const endpoint: UrlEndpoint = {
308
327
  get: async <R = any>(
309
- options?: UrlRequestOptions
328
+ options?: UrlRequestOptions,
310
329
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
330
+ // === ULTRA FAST PATH: No options, no headers, performance mode, no pending warmup ===
331
+ // Check this BEFORE any await to avoid async overhead
332
+ if (
333
+ self.performanceMode &&
334
+ !cacheConfig?.enabled &&
335
+ !options &&
336
+ !self.targetsPromise
337
+ ) {
338
+ return self._requestFastWithBuffer<R>(
339
+ "GET",
340
+ baseUrl,
341
+ urlBuffer,
342
+ cachedAntibot,
343
+ );
344
+ }
345
+
311
346
  // Wait for targets to complete if still pending
312
347
  if (self.targetsPromise) {
313
348
  await self.targetsPromise;
314
349
  }
315
350
 
316
- // Check if caching is enabled for this URL
317
- const cacheConfig = self.cacheConfig.get(prop);
351
+ const useAntibot = options?.antibot ?? cachedAntibot;
318
352
 
319
- // Check if antibot is enabled for this URL (from targets config or per-request)
320
- const useAntibot =
321
- options?.antibot ?? self.antibotConfig.get(prop) ?? false;
353
+ // === FAST PATH after warmup ===
354
+ if (self.performanceMode && !cacheConfig?.enabled && !options) {
355
+ return self._requestFastWithBuffer<R>(
356
+ "GET",
357
+ baseUrl,
358
+ urlBuffer,
359
+ useAntibot,
360
+ );
361
+ }
322
362
 
323
363
  // === FAST PATH: Skip overhead when no cache and performance mode ===
324
364
  if (!cacheConfig?.enabled && self.performanceMode) {
@@ -331,10 +371,8 @@ export class JirenClient<
331
371
  });
332
372
  }
333
373
 
334
- // === SLOW PATH: Full features (cache, metrics, dedupe) ===
335
374
  const startTime = performance.now();
336
375
 
337
- // Try L1 cache first
338
376
  if (cacheConfig?.enabled) {
339
377
  const cached = self.cache.get(baseUrl, options?.path, options);
340
378
  if (cached) {
@@ -365,7 +403,7 @@ export class JirenClient<
365
403
  // ** Deduplication Logic **
366
404
  // Create a unique key for this request based on URL and critical options
367
405
  const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(
368
- options?.headers || {}
406
+ options?.headers || {},
369
407
  )}`;
370
408
 
371
409
  // Check if there is already an identical request in flight
@@ -408,7 +446,7 @@ export class JirenClient<
408
446
  responseType: options?.responseType,
409
447
  antibot: useAntibot,
410
448
  timeout: options?.timeout,
411
- }
449
+ },
412
450
  );
413
451
 
414
452
  // Store in cache if enabled
@@ -422,7 +460,7 @@ export class JirenClient<
422
460
  response as JirenResponse,
423
461
  cacheConfig.ttl,
424
462
  options?.path,
425
- options
463
+ options,
426
464
  );
427
465
  }
428
466
 
@@ -452,7 +490,6 @@ export class JirenClient<
452
490
 
453
491
  return response;
454
492
  } catch (error) {
455
- // Record failed request (skip in performance mode)
456
493
  if (!self.performanceMode) {
457
494
  const responseTimeMs = performance.now() - startTime;
458
495
  self.metricsCollector.recordRequest(prop, {
@@ -483,11 +520,11 @@ export class JirenClient<
483
520
  },
484
521
 
485
522
  post: async <R = any>(
486
- options?: UrlRequestOptions
523
+ options?: UrlRequestOptions,
487
524
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
488
525
  const { headers, serializedBody } = self.prepareBody(
489
526
  options?.body,
490
- options?.headers
527
+ options?.headers,
491
528
  );
492
529
  return self._request<R>(
493
530
  "POST",
@@ -497,16 +534,16 @@ export class JirenClient<
497
534
  headers,
498
535
  maxRedirects: options?.maxRedirects,
499
536
  responseType: options?.responseType,
500
- }
537
+ },
501
538
  );
502
539
  },
503
540
 
504
541
  put: async <R = any>(
505
- options?: UrlRequestOptions
542
+ options?: UrlRequestOptions,
506
543
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
507
544
  const { headers, serializedBody } = self.prepareBody(
508
545
  options?.body,
509
- options?.headers
546
+ options?.headers,
510
547
  );
511
548
  return self._request<R>(
512
549
  "PUT",
@@ -516,16 +553,16 @@ export class JirenClient<
516
553
  headers,
517
554
  maxRedirects: options?.maxRedirects,
518
555
  responseType: options?.responseType,
519
- }
556
+ },
520
557
  );
521
558
  },
522
559
 
523
560
  patch: async <R = any>(
524
- options?: UrlRequestOptions
561
+ options?: UrlRequestOptions,
525
562
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
526
563
  const { headers, serializedBody } = self.prepareBody(
527
564
  options?.body,
528
- options?.headers
565
+ options?.headers,
529
566
  );
530
567
  return self._request<R>(
531
568
  "PATCH",
@@ -535,16 +572,16 @@ export class JirenClient<
535
572
  headers,
536
573
  maxRedirects: options?.maxRedirects,
537
574
  responseType: options?.responseType,
538
- }
575
+ },
539
576
  );
540
577
  },
541
578
 
542
579
  delete: async <R = any>(
543
- options?: UrlRequestOptions
580
+ options?: UrlRequestOptions,
544
581
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
545
582
  const { headers, serializedBody } = self.prepareBody(
546
583
  options?.body,
547
- options?.headers
584
+ options?.headers,
548
585
  );
549
586
  return self._request<R>(
550
587
  "DELETE",
@@ -554,12 +591,12 @@ export class JirenClient<
554
591
  headers,
555
592
  maxRedirects: options?.maxRedirects,
556
593
  responseType: options?.responseType,
557
- }
594
+ },
558
595
  );
559
596
  },
560
597
 
561
598
  head: async (
562
- options?: UrlRequestOptions
599
+ options?: UrlRequestOptions,
563
600
  ): Promise<JirenResponse<any>> => {
564
601
  return self._request("HEAD", buildUrl(options?.path), null, {
565
602
  headers: options?.headers,
@@ -569,7 +606,7 @@ export class JirenClient<
569
606
  },
570
607
 
571
608
  options: async (
572
- options?: UrlRequestOptions
609
+ options?: UrlRequestOptions,
573
610
  ): Promise<JirenResponse<any>> => {
574
611
  return self._request("OPTIONS", buildUrl(options?.path), null, {
575
612
  headers: options?.headers,
@@ -578,95 +615,116 @@ export class JirenClient<
578
615
  });
579
616
  },
580
617
 
581
- /**
582
- * Prefetch/refresh cache for this URL
583
- * Clears existing cache and makes a fresh request
584
- */
585
- prefetch: async (options?: UrlRequestOptions): Promise<void> => {
586
- // Clear cache for this URL
587
- self.cache.clear(baseUrl);
588
-
589
- // Make fresh request to populate cache
590
- const cacheConfig = self.cacheConfig.get(prop);
591
- if (cacheConfig?.enabled) {
592
- await self._request("GET", buildUrl(options?.path), null, {
593
- headers: options?.headers,
594
- maxRedirects: options?.maxRedirects,
595
- antibot: options?.antibot,
596
- });
597
- }
598
- },
599
-
600
- /**
601
- * 🚀 SIMD-ACCELERATED: Get JSON fields in a single native call
602
- * Combines HTTP request + JSON parsing in one FFI call for maximum speed.
603
- * Uses SIMD-accelerated key matching for 2-4x faster field extraction.
604
- *
605
- * @param fields - Array of field names to extract from the JSON response
606
- * @param options - Request options (path, headers, etc.)
607
- * @returns Object with requested fields (similar to Partial<T>)
608
- *
609
- * @example
610
- * const { hello } = await client.url.api.getJsonFields(['hello']);
611
- * console.log(hello); // "world"
612
- */
613
- getJsonFields: async <T extends Record<string, any>>(
614
- fields: (keyof T)[],
615
- options?: UrlRequestOptions
616
- ): Promise<Partial<T>> => {
617
- // Wait for targets to complete if still pending
618
- if (self.targetsPromise) {
619
- await self.targetsPromise;
618
+ getJsonFields: async <R = any>(
619
+ fields: string[],
620
+ options?: UrlRequestOptions,
621
+ ): Promise<R> => {
622
+ if (fields.length > 8) {
623
+ throw new Error("Maximum 8 fields supported per call");
620
624
  }
621
625
 
622
626
  const url = buildUrl(options?.path);
627
+ const urlBuffer = self.getUrlBuffer(url);
623
628
 
624
- // For the combined FFI, we need to pass field names as char**
625
- // Simplest approach: create null-terminated field buffers and store pointers
626
- const fieldCount = Math.min(fields.length, 8); // Max 8 fields per native limit
627
-
628
- // Create field name buffers (must keep references alive)
629
+ // Prepare field names as C cstrings array
630
+ const fieldPointers = new BigUint64Array(fields.length);
629
631
  const fieldBuffers: Buffer[] = [];
630
- for (let i = 0; i < fieldCount; i++) {
631
- fieldBuffers.push(Buffer.from(String(fields[i]) + "\0"));
632
+ for (let i = 0; i < fields.length; i++) {
633
+ const buf = Buffer.from(fields[i] + "\0");
634
+ fieldBuffers.push(buf);
635
+ fieldPointers[i] = BigInt(ptr(buf));
632
636
  }
633
637
 
634
- // Build the response by making the request and parsing on TS side
635
- // (The native combined function requires char** which is complex in Bun FFI)
636
- // Instead, we use the existing fast path + native field extraction
637
- const response = await self._request<any>("GET", url, null, {
638
- headers: options?.headers,
639
- maxRedirects: options?.maxRedirects,
640
- responseType: "json",
641
- });
638
+ let resultPtr: Pointer | null = null;
642
639
 
643
- // Extract only the requested fields from the response
644
- const result: Partial<T> = {};
645
- const data = response.body ? await response.body.json() : response;
640
+ if (options?.headers || options?.body) {
641
+ // Standard request with JSON extraction
642
+ const { headers, serializedBody } = self.prepareBody(
643
+ options.body,
644
+ options.headers,
645
+ );
646
+ const methodBuffer = self.methodBuffers["GET"]!; // Default to GET for now
647
+ const headersBuffer = Buffer.from(
648
+ Object.entries({ ...self.defaultHeaders, ...headers })
649
+ .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
650
+ .join("\r\n") + "\0",
651
+ );
652
+ const bodyBuffer = serializedBody
653
+ ? Buffer.from(serializedBody + "\0")
654
+ : null;
655
+
656
+ resultPtr = await self._requestJsonFields(
657
+ self.ptr!,
658
+ methodBuffer,
659
+ urlBuffer,
660
+ headersBuffer,
661
+ bodyBuffer,
662
+ fieldPointers as any,
663
+ fields.length,
664
+ );
665
+ } else {
666
+ // Optimized GET request
667
+ resultPtr = await self._getJsonFields(
668
+ self.ptr!,
669
+ urlBuffer,
670
+ fieldPointers as any,
671
+ fields.length,
672
+ );
673
+ }
646
674
 
647
- for (const field of fields) {
648
- const key = String(field);
649
- if (key in data) {
650
- (result as any)[key] = data[key];
675
+ if (!resultPtr) {
676
+ throw new Error("Native getJsonFields failed");
677
+ }
678
+
679
+ try {
680
+ const result: any = {};
681
+ for (let i = 0; i < fields.length; i++) {
682
+ const key = fields[i] as string;
683
+ const type = self._fieldType(resultPtr as any, i);
684
+ if (type === 0) {
685
+ // null
686
+ result[key] = null;
687
+ } else if (type === 1) {
688
+ // bool
689
+ const len = Number(self._fieldLen(resultPtr as any, i));
690
+ result[key] = len === 1;
691
+ } else if (type === 2) {
692
+ // number
693
+ const len = Number(self._fieldLen(resultPtr as any, i));
694
+ result[key] = len; // Note: current FFI stores number as len for simplicity
695
+ } else if (type === 3) {
696
+ // string
697
+ const ptrVal = self._fieldStr(resultPtr as any, i);
698
+ const lenVal = Number(self._fieldLen(resultPtr as any, i));
699
+ if (ptrVal && lenVal > 0) {
700
+ const bytes = toArrayBuffer(ptrVal, 0, lenVal);
701
+ result[key] = self.decoder.decode(bytes);
702
+ } else {
703
+ result[key] = "";
704
+ }
705
+ }
651
706
  }
707
+ return result as R;
708
+ } finally {
709
+ self._jsonFieldsFree(resultPtr as any);
652
710
  }
711
+ },
712
+
713
+ prefetch: async (options?: UrlRequestOptions): Promise<void> => {
714
+ self.cache.clear(baseUrl);
653
715
 
654
- return result;
716
+ const cacheConfig = self.cacheConfig.get(prop);
717
+ if (cacheConfig?.enabled) {
718
+ await self._request("GET", buildUrl(options?.path), null, {
719
+ headers: options?.headers,
720
+ maxRedirects: options?.maxRedirects,
721
+ antibot: options?.antibot,
722
+ });
723
+ }
655
724
  },
656
725
 
657
- /**
658
- * Download with native progress tracking
659
- * Note: Currently supports HTTP only. HTTPS uses the fast non-streaming path.
660
- * @example
661
- * const response = await client.url.cdn.download({
662
- * path: '/large-file.zip',
663
- * onDownloadProgress: (progress) => {
664
- * console.log(`${progress.percent}% @ ${progress.speed / 1024} KB/s`);
665
- * }
666
- * });
667
- */
668
726
  download: async <R = any>(
669
- options?: ProgressRequestOptions
727
+ options?: ProgressRequestOptions,
670
728
  ): Promise<JirenResponse<R>> => {
671
729
  const url = buildUrl(options?.path);
672
730
 
@@ -719,7 +777,7 @@ export class JirenClient<
719
777
  ? Buffer.from(
720
778
  Object.entries(options.headers)
721
779
  .map(([k, v]) => `${k}: ${v}`)
722
- .join("\r\n") + "\0"
780
+ .join("\r\n") + "\0",
723
781
  )
724
782
  : null;
725
783
 
@@ -730,7 +788,7 @@ export class JirenClient<
730
788
  urlBuffer,
731
789
  headersBuffer,
732
790
  null,
733
- null // No native callback, we poll
791
+ null, // No native callback, we poll
734
792
  );
735
793
 
736
794
  if (!streamPtr) {
@@ -747,10 +805,10 @@ export class JirenClient<
747
805
  lib.symbols.zstream_poll(streamPtr);
748
806
 
749
807
  const loaded = Number(
750
- lib.symbols.zstream_bytes_received(streamPtr)
808
+ lib.symbols.zstream_bytes_received(streamPtr),
751
809
  );
752
810
  const total = Number(
753
- lib.symbols.zstream_content_length(streamPtr)
811
+ lib.symbols.zstream_content_length(streamPtr),
754
812
  );
755
813
 
756
814
  if (loaded > lastLoaded) {
@@ -780,10 +838,10 @@ export class JirenClient<
780
838
 
781
839
  // Final progress event
782
840
  const finalLoaded = Number(
783
- lib.symbols.zstream_bytes_received(streamPtr)
841
+ lib.symbols.zstream_bytes_received(streamPtr),
784
842
  );
785
843
  const finalTotal = Number(
786
- lib.symbols.zstream_content_length(streamPtr)
844
+ lib.symbols.zstream_content_length(streamPtr),
787
845
  );
788
846
  options.onDownloadProgress({
789
847
  loaded: finalLoaded,
@@ -798,7 +856,7 @@ export class JirenClient<
798
856
  const bodyLen = Number(lib.symbols.zstream_body_len(streamPtr));
799
857
  const bodyPtr = lib.symbols.zstream_body(streamPtr);
800
858
  const headersLen = Number(
801
- lib.symbols.zstream_headers_len(streamPtr)
859
+ lib.symbols.zstream_headers_len(streamPtr),
802
860
  );
803
861
  const headersPtr = lib.symbols.zstream_headers(streamPtr);
804
862
 
@@ -842,13 +900,6 @@ export class JirenClient<
842
900
  bodyUsed = true;
843
901
  return JSON.parse(decoder.decode(bodyBuffer));
844
902
  },
845
- jsonFields: async <Fields extends Record<string, any> = any>(
846
- fields: (keyof Fields)[]
847
- ) => {
848
- bodyUsed = true;
849
- const dec = new TextDecoder();
850
- return parseJsonFields<Fields>(dec.decode(bodyBuffer), fields);
851
- },
852
903
  };
853
904
 
854
905
  Object.defineProperty(bodyObj, "bodyUsed", {
@@ -867,26 +918,11 @@ export class JirenClient<
867
918
  } as JirenResponse<R>;
868
919
  },
869
920
 
870
- /**
871
- * Upload with progress tracking
872
- * @param method - HTTP method (POST, PUT, PATCH)
873
- * @param body - Request body (string or object)
874
- * @param options - Request options with onUploadProgress callback
875
- * @example
876
- * await client.url.api.upload({
877
- * method: 'POST',
878
- * body: largeData,
879
- * path: '/upload',
880
- * onUploadProgress: (progress) => {
881
- * console.log(`${progress.percent}% @ ${progress.speed / 1024} KB/s`);
882
- * }
883
- * });
884
- */
885
921
  upload: async <R = any>(
886
922
  options?: ProgressRequestOptions & {
887
923
  method?: "POST" | "PUT" | "PATCH";
888
924
  body?: string | object | null;
889
- }
925
+ },
890
926
  ): Promise<JirenResponse<R>> => {
891
927
  const url = buildUrl(options?.path);
892
928
  const method = options?.method || "POST";
@@ -927,8 +963,6 @@ export class JirenClient<
927
963
  const isHttps = url.startsWith("https://");
928
964
 
929
965
  if (isHttps) {
930
- // HTTPS: Use fast path with simulated progress
931
- // (Native layer handles the upload in one call)
932
966
  const { headers: preparedHeaders, serializedBody } =
933
967
  self.prepareBody(options.body, options.headers);
934
968
 
@@ -938,7 +972,7 @@ export class JirenClient<
938
972
  serializedBody,
939
973
  {
940
974
  headers: preparedHeaders,
941
- }
975
+ },
942
976
  );
943
977
 
944
978
  // Fire final progress event (100%)
@@ -958,7 +992,7 @@ export class JirenClient<
958
992
  // HTTP: Use native Zig streaming for upload with progress
959
993
  const { headers: preparedHeaders } = self.prepareBody(
960
994
  options.body,
961
- options.headers
995
+ options.headers,
962
996
  );
963
997
 
964
998
  // Build headers string for native
@@ -979,7 +1013,7 @@ export class JirenClient<
979
1013
  headersStr ? Buffer.from(headersStr + "\0") : null,
980
1014
  bodyBuffer,
981
1015
  bodyLength,
982
- null // We'll poll for progress instead of using callback
1016
+ null, // We'll poll for progress instead of using callback
983
1017
  );
984
1018
 
985
1019
  if (!uploadStreamPtr) {
@@ -1010,7 +1044,7 @@ export class JirenClient<
1010
1044
  if (sent === 0n) break;
1011
1045
 
1012
1046
  const bytesSent = Number(
1013
- lib.symbols.zupload_bytes_sent(uploadStreamPtr)
1047
+ lib.symbols.zupload_bytes_sent(uploadStreamPtr),
1014
1048
  );
1015
1049
 
1016
1050
  // Calculate progress
@@ -1045,12 +1079,12 @@ export class JirenClient<
1045
1079
  const bodyPtr =
1046
1080
  lib.symbols.zupload_response_body(uploadStreamPtr);
1047
1081
  const bodyLen = Number(
1048
- lib.symbols.zupload_response_body_len(uploadStreamPtr)
1082
+ lib.symbols.zupload_response_body_len(uploadStreamPtr),
1049
1083
  );
1050
1084
  const headersPtr =
1051
1085
  lib.symbols.zupload_response_headers(uploadStreamPtr);
1052
1086
  const headersLen = Number(
1053
- lib.symbols.zupload_response_headers_len(uploadStreamPtr)
1087
+ lib.symbols.zupload_response_headers_len(uploadStreamPtr),
1054
1088
  );
1055
1089
 
1056
1090
  // Copy data
@@ -1061,7 +1095,7 @@ export class JirenClient<
1061
1095
  const responseHeaders =
1062
1096
  headersLen > 0 && headersPtr
1063
1097
  ? Buffer.from(
1064
- toArrayBuffer(headersPtr, 0, headersLen)
1098
+ toArrayBuffer(headersPtr, 0, headersLen),
1065
1099
  ).toString()
1066
1100
  : "";
1067
1101
 
@@ -1088,16 +1122,7 @@ export class JirenClient<
1088
1122
  bodyUsed = true;
1089
1123
  return JSON.parse(bodyData);
1090
1124
  },
1091
- jsonFields: async <Fields extends Record<string, any> = any>(
1092
- fields: (keyof Fields)[]
1093
- ) => {
1094
- bodyUsed = true;
1095
- const dec = new TextDecoder();
1096
- return parseJsonFields<Fields>(
1097
- dec.decode(bodyBuffer),
1098
- fields
1099
- );
1100
- },
1125
+
1101
1126
  arrayBuffer: async () => {
1102
1127
  bodyUsed = true;
1103
1128
  return new TextEncoder().encode(bodyData)
@@ -1127,7 +1152,11 @@ export class JirenClient<
1127
1152
  lib.symbols.zupload_free(uploadStreamPtr);
1128
1153
  }
1129
1154
  },
1130
- } as UrlEndpoint;
1155
+ };
1156
+
1157
+ // Cache and return the endpoint
1158
+ self.endpointCache.set(prop, endpoint);
1159
+ return endpoint;
1131
1160
  },
1132
1161
  });
1133
1162
  }
@@ -1167,8 +1196,8 @@ export class JirenClient<
1167
1196
  const urlBuffer = Buffer.from(url + "\0");
1168
1197
  lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
1169
1198
  resolve();
1170
- })
1171
- )
1199
+ }),
1200
+ ),
1172
1201
  );
1173
1202
  }
1174
1203
 
@@ -1186,41 +1215,103 @@ export class JirenClient<
1186
1215
  this.preconnect(urls);
1187
1216
  }
1188
1217
 
1218
+ private _requestFast<T = any>(
1219
+ method: string,
1220
+ url: string,
1221
+ antibot: boolean = false,
1222
+ ): JirenResponse<T> {
1223
+ if (!this.ptr) throw new Error("Client is closed");
1224
+
1225
+ // Use pre-computed buffers where possible
1226
+ const methodBuffer =
1227
+ this.methodBuffers[method] || Buffer.from(method + "\0");
1228
+ const urlBuffer = this.getUrlBuffer(url);
1229
+
1230
+ // Direct FFI call - no async wrapper
1231
+ const respPtr = this._requestFull(
1232
+ this.ptr,
1233
+ methodBuffer,
1234
+ urlBuffer,
1235
+ this.defaultHeadersBuffer,
1236
+ null, // no body
1237
+ 5, // default max redirects
1238
+ antibot,
1239
+ );
1240
+
1241
+ if (!respPtr) {
1242
+ throw new Error("Native request failed (returned null pointer)");
1243
+ }
1244
+
1245
+ return this.parseResponseFull<T>(respPtr as any, url);
1246
+ }
1247
+
1248
+ /**
1249
+ * Ultra-fast synchronous request with pre-computed URL buffer
1250
+ * Zero allocation overhead in hot path
1251
+ */
1252
+ private _requestFastWithBuffer<T = any>(
1253
+ method: string,
1254
+ url: string,
1255
+ urlBuffer: Buffer,
1256
+ antibot: boolean = false,
1257
+ ): JirenResponse<T> {
1258
+ if (!this.ptr) throw new Error("Client is closed");
1259
+
1260
+ const methodBuffer =
1261
+ this.methodBuffers[method] || Buffer.from(method + "\0");
1262
+
1263
+ const respPtr = this._requestFull(
1264
+ this.ptr,
1265
+ methodBuffer,
1266
+ urlBuffer,
1267
+ this.defaultHeadersBuffer,
1268
+ null,
1269
+ 5,
1270
+ antibot,
1271
+ );
1272
+
1273
+ if (!respPtr) {
1274
+ throw new Error("Native request failed (returned null pointer)");
1275
+ }
1276
+
1277
+ return this.parseResponseFull<T>(respPtr as any, url);
1278
+ }
1279
+
1189
1280
  protected async _request<T = any>(
1190
1281
  method: string,
1191
1282
  url: string,
1192
1283
  body?: string | null,
1193
- options?: RequestOptions & { responseType: "json" }
1284
+ options?: RequestOptions & { responseType: "json" },
1194
1285
  ): Promise<T>;
1195
1286
  protected async _request<T = any>(
1196
1287
  method: string,
1197
1288
  url: string,
1198
1289
  body?: string | null,
1199
- options?: RequestOptions & { responseType: "text" }
1290
+ options?: RequestOptions & { responseType: "text" },
1200
1291
  ): Promise<string>;
1201
1292
  protected async _request<T = any>(
1202
1293
  method: string,
1203
1294
  url: string,
1204
1295
  body?: string | null,
1205
- options?: RequestOptions & { responseType: "arraybuffer" }
1296
+ options?: RequestOptions & { responseType: "arraybuffer" },
1206
1297
  ): Promise<ArrayBuffer>;
1207
1298
  protected async _request<T = any>(
1208
1299
  method: string,
1209
1300
  url: string,
1210
1301
  body?: string | null,
1211
- options?: RequestOptions & { responseType: "blob" }
1302
+ options?: RequestOptions & { responseType: "blob" },
1212
1303
  ): Promise<Blob>;
1213
1304
  protected async _request<T = any>(
1214
1305
  method: string,
1215
1306
  url: string,
1216
1307
  body?: string | null,
1217
- options?: RequestOptions
1308
+ options?: RequestOptions,
1218
1309
  ): Promise<JirenResponse<T>>;
1219
1310
  protected async _request<T = any>(
1220
1311
  method: string,
1221
1312
  url: string,
1222
1313
  body?: string | null,
1223
- options?: RequestOptions | Record<string, string> | null
1314
+ options?: RequestOptions | Record<string, string> | null,
1224
1315
  ): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
1225
1316
  if (!this.ptr) throw new Error("Client is closed");
1226
1317
 
@@ -1269,7 +1360,7 @@ export class JirenClient<
1269
1360
  // Use pre-computed method buffer or create one for custom methods
1270
1361
  const methodBuffer =
1271
1362
  this.methodBuffers[method] || Buffer.from(method + "\0");
1272
- const urlBuffer = Buffer.from(url + "\0");
1363
+ const urlBuffer = this.getUrlBuffer(url);
1273
1364
 
1274
1365
  let bodyBuffer: Buffer | null = null;
1275
1366
  if (body) {
@@ -1330,7 +1421,7 @@ export class JirenClient<
1330
1421
  // If it has 'responseType', 'method', etc., it's RequestOptions.
1331
1422
  // Simpler: Just check if 'retry' is number or object.
1332
1423
  if (options && typeof options === "object" && "retry" in options) {
1333
- const userRetry = (options as any).retry;
1424
+ const userRetry = options?.retry;
1334
1425
  if (typeof userRetry === "number") {
1335
1426
  retryConfig = { count: userRetry, delay: 100, backoff: 2 };
1336
1427
  } else if (typeof userRetry === "object") {
@@ -1353,15 +1444,15 @@ export class JirenClient<
1353
1444
  const makeRequest = async (): Promise<
1354
1445
  JirenResponse<T> | T | string | ArrayBuffer | Blob
1355
1446
  > => {
1356
- // Use optimized single-call API (reduces 5 FFI calls to 1)
1357
- const respPtr = lib.symbols.zclient_request_full(
1447
+ // Use optimized single-call API with cached FFI symbol
1448
+ const respPtr = await this._requestFull(
1358
1449
  this.ptr,
1359
1450
  methodBuffer,
1360
1451
  urlBuffer,
1361
1452
  headersBuffer,
1362
1453
  bodyBuffer,
1363
1454
  maxRedirects,
1364
- antibot
1455
+ antibot,
1365
1456
  );
1366
1457
 
1367
1458
  if (!respPtr) {
@@ -1436,7 +1527,7 @@ export class JirenClient<
1436
1527
 
1437
1528
  private parseResponse<T = any>(
1438
1529
  respPtr: Pointer | null,
1439
- url: string
1530
+ url: string,
1440
1531
  ): JirenResponse<T> {
1441
1532
  if (!respPtr)
1442
1533
  throw new Error("Native request failed (returned null pointer)");
@@ -1447,7 +1538,7 @@ export class JirenClient<
1447
1538
  const bodyPtr = lib.symbols.zclient_response_body(respPtr);
1448
1539
 
1449
1540
  const headersLen = Number(
1450
- lib.symbols.zclient_response_headers_len(respPtr)
1541
+ lib.symbols.zclient_response_headers_len(respPtr),
1451
1542
  );
1452
1543
  let headersObj: Record<string, string> | NativeHeaders = {};
1453
1544
 
@@ -1477,31 +1568,26 @@ export class JirenClient<
1477
1568
  }
1478
1569
  return Reflect.get(target, prop);
1479
1570
  },
1480
- }
1571
+ },
1481
1572
  ) as unknown as Record<string, string>; // Lie to TS
1482
1573
 
1483
1574
  let buffer: ArrayBuffer | Buffer = new ArrayBuffer(0);
1484
1575
  if (len > 0 && bodyPtr) {
1485
- // Create a copy of the buffer because the native response is freed immediately after
1486
1576
  buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
1487
1577
 
1488
- // Handle GZIP decompression if needed
1489
1578
  const bufferView = new Uint8Array(buffer);
1490
- // Check for gzip magic bytes (0x1f 0x8b)
1491
1579
  if (
1492
1580
  bufferView.length >= 2 &&
1493
1581
  bufferView[0] === 0x1f &&
1494
1582
  bufferView[1] === 0x8b
1495
1583
  ) {
1496
1584
  try {
1497
- // Use Bun's built-in gzip decompression
1498
1585
  const decompressed = Bun.gunzipSync(bufferView);
1499
1586
  buffer = decompressed.buffer.slice(
1500
1587
  decompressed.byteOffset,
1501
- decompressed.byteOffset + decompressed.byteLength
1588
+ decompressed.byteOffset + decompressed.byteLength,
1502
1589
  );
1503
1590
  } catch (e) {
1504
- // Decompression failed, keep original buffer
1505
1591
  console.warn("[Jiren] gzip decompression failed:", e);
1506
1592
  }
1507
1593
  }
@@ -1522,7 +1608,7 @@ export class JirenClient<
1522
1608
  const buf = buffer as Buffer;
1523
1609
  return buf.buffer.slice(
1524
1610
  buf.byteOffset,
1525
- buf.byteOffset + buf.byteLength
1611
+ buf.byteOffset + buf.byteLength,
1526
1612
  ) as ArrayBuffer;
1527
1613
  }
1528
1614
  return buffer as ArrayBuffer;
@@ -1539,16 +1625,9 @@ export class JirenClient<
1539
1625
  consumeBody();
1540
1626
  const text = this.decoder.decode(buffer);
1541
1627
  // Note: JS JSON.parse is fastest for full object conversion
1542
- // Native SIMD acceleration benefits field extraction (jsonFields method)
1628
+
1543
1629
  return JSON.parse(text);
1544
1630
  },
1545
- jsonFields: async <Fields extends Record<string, any> = any>(
1546
- fields: (keyof Fields)[]
1547
- ) => {
1548
- consumeBody();
1549
- const text = this.decoder.decode(buffer);
1550
- return parseJsonFields<Fields>(text, fields);
1551
- },
1552
1631
  };
1553
1632
 
1554
1633
  // Update bodyUsed getter to reflect local variable
@@ -1576,71 +1655,92 @@ export class JirenClient<
1576
1655
  */
1577
1656
  private parseResponseFull<T = any>(
1578
1657
  respPtr: Pointer | null,
1579
- url: string
1658
+ url: string,
1580
1659
  ): JirenResponse<T> {
1581
1660
  if (!respPtr)
1582
1661
  throw new Error("Native request failed (returned null pointer)");
1583
1662
 
1584
1663
  try {
1585
- // Use FFI accessor functions (avoids BigInt-to-Pointer conversion issues)
1586
- const status = lib.symbols.zfull_response_status(respPtr);
1587
- const bodyPtr = lib.symbols.zfull_response_body(respPtr);
1588
- const bodyLen = Number(lib.symbols.zfull_response_body_len(respPtr));
1589
- const headersPtr = lib.symbols.zfull_response_headers(respPtr);
1590
- const headersLen = Number(
1591
- lib.symbols.zfull_response_headers_len(respPtr)
1592
- );
1593
-
1594
- let headersObj: Record<string, string> | NativeHeaders = {};
1664
+ // Use cached FFI symbol references (avoids property lookup overhead)
1665
+ const status = this._fullResponseStatus(respPtr);
1666
+ const bodyPtr = this._fullResponseBody(respPtr);
1667
+ const bodyLen = Number(this._fullResponseBodyLen(respPtr));
1668
+
1669
+ // === SMART LAZY HEADERS: Copy data now, parse only when accessed ===
1670
+ // This ensures headers always work while minimizing parsing overhead
1671
+ const headersPtr = this._fullResponseHeaders(respPtr);
1672
+ const headersLen = Number(this._fullResponseHeadersLen(respPtr));
1673
+ let rawHeadersData: Uint8Array | null = null;
1595
1674
  if (headersLen > 0 && headersPtr) {
1596
- const rawSrc = toArrayBuffer(headersPtr, 0, headersLen);
1597
- const raw = new Uint8Array(rawSrc.slice(0));
1598
- headersObj = new NativeHeaders(raw);
1675
+ rawHeadersData = new Uint8Array(
1676
+ toArrayBuffer(headersPtr, 0, headersLen).slice(0),
1677
+ );
1599
1678
  }
1600
1679
 
1601
- // Simplified proxy for performance mode
1602
- const headersProxy = this.performanceMode
1603
- ? (headersObj as Record<string, string>)
1604
- : (new Proxy(headersObj instanceof NativeHeaders ? headersObj : {}, {
1605
- get(target, prop) {
1606
- if (target instanceof NativeHeaders && typeof prop === "string") {
1607
- if (prop === "toJSON") return () => target.toJSON();
1608
- const val = target.get(prop);
1609
- if (val !== null) return val;
1680
+ // Lazy headers - only parse when first accessed
1681
+ let parsedHeaders: Record<string, string> | null = null;
1682
+ const decoder = this.decoder;
1683
+ const headersProxy = new Proxy({} as Record<string, string>, {
1684
+ get(_, prop: string) {
1685
+ if (!parsedHeaders) {
1686
+ parsedHeaders = {};
1687
+ if (rawHeadersData) {
1688
+ const headersStr = decoder.decode(rawHeadersData);
1689
+ for (const line of headersStr.split("\r\n")) {
1690
+ const colonIdx = line.indexOf(":");
1691
+ if (colonIdx > 0) {
1692
+ const key = line.slice(0, colonIdx).toLowerCase();
1693
+ const value = line.slice(colonIdx + 1).trim();
1694
+ parsedHeaders[key] = value;
1695
+ }
1610
1696
  }
1611
- return Reflect.get(target, prop);
1612
- },
1613
- }) as unknown as Record<string, string>);
1697
+ }
1698
+ }
1699
+ return parsedHeaders[prop];
1700
+ },
1701
+ has(_, prop: string) {
1702
+ // Trigger lazy parsing if needed
1703
+ if (!parsedHeaders) headersProxy[prop];
1704
+ return prop in (parsedHeaders || {});
1705
+ },
1706
+ ownKeys() {
1707
+ // Trigger lazy parsing if needed
1708
+ if (!parsedHeaders) headersProxy[""];
1709
+ return Object.keys(parsedHeaders || {});
1710
+ },
1711
+ });
1614
1712
 
1615
1713
  let buffer: ArrayBuffer | Buffer = new ArrayBuffer(0);
1616
1714
  if (bodyLen > 0 && bodyPtr) {
1617
- buffer = toArrayBuffer(bodyPtr, 0, bodyLen).slice(0);
1618
-
1619
- // Handle GZIP decompression - check content-encoding header first
1620
- const contentEncoding =
1621
- headersObj instanceof NativeHeaders
1622
- ? headersObj.get("content-encoding")?.toLowerCase()
1623
- : (headersObj as Record<string, string>)[
1624
- "content-encoding"
1625
- ]?.toLowerCase();
1626
-
1627
- const bufferView = new Uint8Array(buffer);
1628
-
1629
- // Only attempt gzip if content-encoding is gzip AND magic bytes match
1630
- if (
1631
- contentEncoding === "gzip" &&
1632
- bufferView.length >= 2 &&
1633
- bufferView[0] === 0x1f &&
1634
- bufferView[1] === 0x8b
1635
- ) {
1636
- try {
1637
- const decompressed = Bun.gunzipSync(bufferView);
1638
- buffer = decompressed.buffer.slice(
1639
- decompressed.byteOffset,
1640
- decompressed.byteOffset + decompressed.byteLength
1641
- );
1642
- } catch {
1643
- // Silently ignore decompression failures - data may already be decompressed
1715
+ const rawBuffer = toArrayBuffer(bodyPtr, 0, bodyLen);
1716
+ const wasDecompressed = this._fullResponseWasDecompressed(respPtr);
1717
+
1718
+ // If native code already decompressed it, use it directly (zero-copy copy)
1719
+ if (wasDecompressed === 1) {
1720
+ buffer = rawBuffer.slice(0);
1721
+ } else {
1722
+ const bufferView = new Uint8Array(rawBuffer);
1723
+
1724
+ // Quick gzip check using magic bytes only (0x1f 0x8b) - faster than checking headers
1725
+ if (
1726
+ bufferView.length >= 2 &&
1727
+ bufferView[0] === 0x1f &&
1728
+ bufferView[1] === 0x8b
1729
+ ) {
1730
+ try {
1731
+ // Use Bun's built-in gzip decompression as fallback if native skip it
1732
+ const decompressed = Bun.gunzipSync(bufferView);
1733
+ buffer = decompressed.buffer.slice(
1734
+ decompressed.byteOffset,
1735
+ decompressed.byteOffset + decompressed.byteLength,
1736
+ );
1737
+ } catch {
1738
+ // Not gzip or decompression failed, use raw copy
1739
+ buffer = rawBuffer.slice(0);
1740
+ }
1741
+ } else {
1742
+ // Not gzip - must copy since we're freeing the response pointer
1743
+ buffer = rawBuffer.slice(0);
1644
1744
  }
1645
1745
  }
1646
1746
  }
@@ -1658,7 +1758,7 @@ export class JirenClient<
1658
1758
  const buf = buffer as Buffer;
1659
1759
  return buf.buffer.slice(
1660
1760
  buf.byteOffset,
1661
- buf.byteOffset + buf.byteLength
1761
+ buf.byteOffset + buf.byteLength,
1662
1762
  ) as ArrayBuffer;
1663
1763
  }
1664
1764
  return buffer as ArrayBuffer;
@@ -1675,13 +1775,6 @@ export class JirenClient<
1675
1775
  consumeBody();
1676
1776
  return JSON.parse(this.decoder.decode(buffer));
1677
1777
  },
1678
- jsonFields: async <Fields extends Record<string, any> = any>(
1679
- fields: (keyof Fields)[]
1680
- ) => {
1681
- consumeBody();
1682
- const text = this.decoder.decode(buffer);
1683
- return parseJsonFields<Fields>(text, fields);
1684
- },
1685
1778
  };
1686
1779
 
1687
1780
  Object.defineProperty(bodyObj, "bodyUsed", { get: () => bodyUsed });
@@ -1697,7 +1790,7 @@ export class JirenClient<
1697
1790
  body: bodyObj,
1698
1791
  } as JirenResponse<T>;
1699
1792
  } finally {
1700
- lib.symbols.zclient_response_full_free(respPtr);
1793
+ this._responseFullFree(respPtr);
1701
1794
  }
1702
1795
  }
1703
1796
 
@@ -1707,7 +1800,7 @@ export class JirenClient<
1707
1800
  */
1708
1801
  private prepareBody(
1709
1802
  body: string | object | null | undefined,
1710
- userHeaders?: Record<string, string>
1803
+ userHeaders?: Record<string, string>,
1711
1804
  ): { headers: Record<string, string>; serializedBody: string | null } {
1712
1805
  let serializedBody: string | null = null;
1713
1806
  const headers = { ...userHeaders };
@@ -1717,7 +1810,7 @@ export class JirenClient<
1717
1810
  serializedBody = JSON.stringify(body);
1718
1811
  // Add Content-Type if not present (case-insensitive check)
1719
1812
  const hasContentType = Object.keys(headers).some(
1720
- (k) => k.toLowerCase() === "content-type"
1813
+ (k) => k.toLowerCase() === "content-type",
1721
1814
  );
1722
1815
  if (!hasContentType) {
1723
1816
  headers["Content-Type"] = "application/json";
@@ -1729,6 +1822,22 @@ export class JirenClient<
1729
1822
 
1730
1823
  return { headers, serializedBody };
1731
1824
  }
1825
+
1826
+ /**
1827
+ * Get or create a cached URL buffer for the given URL.
1828
+ * Caches up to URL_BUFFER_CACHE_MAX entries to avoid per-request allocations.
1829
+ */
1830
+ private getUrlBuffer(url: string): Buffer {
1831
+ let buf = this.urlBufferCache.get(url);
1832
+ if (!buf) {
1833
+ buf = Buffer.from(url + "\0");
1834
+ // Only cache if under limit (prevents unbounded memory growth)
1835
+ if (this.urlBufferCache.size < JirenClient.URL_BUFFER_CACHE_MAX) {
1836
+ this.urlBufferCache.set(url, buf);
1837
+ }
1838
+ }
1839
+ return buf;
1840
+ }
1732
1841
  }
1733
1842
 
1734
1843
  class NativeHeaders {
@@ -1756,7 +1865,7 @@ class NativeHeaders {
1756
1865
  const resPtr = lib.symbols.z_find_header_value(
1757
1866
  this.raw as any,
1758
1867
  this.len,
1759
- keyBuf
1868
+ keyBuf,
1760
1869
  );
1761
1870
 
1762
1871
  if (!resPtr) return null;
@@ -1786,23 +1895,13 @@ class NativeHeaders {
1786
1895
  }
1787
1896
  }
1788
1897
 
1789
- // Fallback for when full object is needed (e.g. debugging)
1790
- // This is expensive as it reparses everything using the old offset method
1791
- // BUT we don't have the offset method easily available on the raw buffer unless we expose a new one?
1792
- // Wait, `zclient_response_parse_header_offsets` takes `Response*`.
1793
- // We don't have Response* anymore.
1794
- // We need `z_parse_header_offsets_from_raw(ptr, len)`.
1795
- // Or just parse in JS since we have the full buffer?
1796
- // Actually, we can just do a JS parser since we have the buffer.
1797
- // It's a fallback anyway.
1798
- // Convert all headers to a JS object using native parsing
1799
1898
  toJSON(): Record<string, string> {
1800
1899
  const obj: Record<string, string> = {};
1801
1900
 
1802
1901
  // Use native batch header parsing (faster than JS string splitting)
1803
1902
  const offsetsPtr = lib.symbols.z_parse_all_headers(
1804
1903
  this.raw as any,
1805
- this.len
1904
+ this.len,
1806
1905
  );
1807
1906
  if (!offsetsPtr) {
1808
1907
  // Fallback to cached values if native parsing fails
@@ -1836,7 +1935,7 @@ class NativeHeaders {
1836
1935
  .decode(this.raw.subarray(nameStart, nameStart + nameLen))
1837
1936
  .toLowerCase();
1838
1937
  const value = this.decoder.decode(
1839
- this.raw.subarray(valueStart, valueStart + valueLen)
1938
+ this.raw.subarray(valueStart, valueStart + valueLen),
1840
1939
  );
1841
1940
 
1842
1941
  obj[key] = value;