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
@@ -62,7 +62,7 @@ export interface JirenClientOptions<
62
62
  performanceMode?: boolean;
63
63
  }
64
64
 
65
- /** Helper to extract keys from Target Config */
65
+ // Helper to extract keys from Target Config
66
66
  export type ExtractTargetKeys<
67
67
  T extends readonly TargetUrlConfig[] | Record<string, UrlConfig>
68
68
  > = T extends readonly TargetUrlConfig[]
@@ -71,7 +71,6 @@ export type ExtractTargetKeys<
71
71
  ? keyof T
72
72
  : never;
73
73
 
74
- /** Type-safe URL accessor - maps keys to UrlEndpoint objects */
75
74
  export type UrlAccessor<
76
75
  T extends readonly TargetUrlConfig[] | Record<string, UrlConfig>
77
76
  > = {
@@ -79,23 +78,7 @@ export type UrlAccessor<
79
78
  };
80
79
 
81
80
  /**
82
- * Helper function to define target URLs with type inference.
83
- * This eliminates the need for 'as const'.
84
- *
85
- * @example
86
- * ```typescript
87
- * const client = new JirenClient({
88
- * targets: defineUrls([
89
- * { key: "google", url: "https://google.com" },
90
- * ])
91
- * });
92
- * // OR
93
- * const client = new JirenClient({
94
- * targets: {
95
- * google: "https://google.com"
96
- * }
97
- * });
98
- * ```
81
+ * Helper to define target URLs with type inference.
99
82
  */
100
83
  export function defineUrls<const T extends readonly TargetUrlConfig[]>(
101
84
  urls: T
@@ -103,11 +86,22 @@ export function defineUrls<const T extends readonly TargetUrlConfig[]>(
103
86
  return urls;
104
87
  }
105
88
 
89
+ // FinalizationRegistry for automatic cleanup when client is garbage collected
90
+ const clientRegistry = new FinalizationRegistry<number>((ptrValue) => {
91
+ // Note: We store the pointer as a number since FinalizationRegistry can't hold Pointer directly
92
+ try {
93
+ lib.symbols.zclient_free(ptrValue as any);
94
+ } catch {
95
+ // Ignore errors during cleanup
96
+ }
97
+ });
98
+
106
99
  export class JirenClient<
107
100
  T extends readonly TargetUrlConfig[] | Record<string, UrlConfig> =
108
101
  | readonly TargetUrlConfig[]
109
102
  | Record<string, UrlConfig>
110
- > {
103
+ > implements Disposable
104
+ {
111
105
  private ptr: Pointer | null;
112
106
  private urlMap: Map<string, string> = new Map();
113
107
  private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
@@ -123,9 +117,9 @@ export class JirenClient<
123
117
  private targetsComplete: Set<string> = new Set();
124
118
  private performanceMode: boolean = false;
125
119
 
126
- // Pre-computed headers (avoid per-request overhead)
120
+ // Pre-computed headers
127
121
  private readonly defaultHeadersStr: string;
128
- private readonly defaultHeadersBuffer: Buffer; // Cached buffer
122
+ private readonly defaultHeadersBuffer: Buffer;
129
123
  private readonly defaultHeaders: Record<string, string> = {
130
124
  "user-agent":
131
125
  "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",
@@ -144,7 +138,7 @@ export class JirenClient<
144
138
  "upgrade-insecure-requests": "1",
145
139
  };
146
140
 
147
- // Pre-computed method buffers (avoid per-request allocation)
141
+ // Pre-computed method buffers
148
142
  private readonly methodBuffers: Record<string, Buffer> = {
149
143
  GET: Buffer.from("GET\0"),
150
144
  POST: Buffer.from("POST\0"),
@@ -155,7 +149,7 @@ export class JirenClient<
155
149
  OPTIONS: Buffer.from("OPTIONS\0"),
156
150
  };
157
151
 
158
- // Reusable TextDecoder (avoid per-response allocation)
152
+ // Reusable TextDecoder
159
153
  private readonly decoder = new TextDecoder();
160
154
 
161
155
  /** Type-safe URL accessor for warmed-up URLs */
@@ -170,7 +164,10 @@ export class JirenClient<
170
164
  this.ptr = lib.symbols.zclient_new();
171
165
  if (!this.ptr) throw new Error("Failed to create native client instance");
172
166
 
173
- // Pre-compute default headers string (avoid per-request overhead)
167
+ // Register for automatic cleanup when this client is garbage collected
168
+ clientRegistry.register(this, this.ptr as unknown as number, this);
169
+
170
+ // Pre-computed default headers string
174
171
  const orderedKeys = [
175
172
  "sec-ch-ua",
176
173
  "sec-ch-ua-mobile",
@@ -647,15 +644,30 @@ export class JirenClient<
647
644
 
648
645
  /**
649
646
  * Free the native client resources.
650
- * Must be called when the client is no longer needed.
647
+ * Note: This is called automatically when the client is garbage collected,
648
+ * or you can use the `using` keyword for automatic cleanup in a scope.
651
649
  */
652
650
  public close(): void {
653
651
  if (this.ptr) {
652
+ // Unregister from FinalizationRegistry since we're manually closing
653
+ clientRegistry.unregister(this);
654
654
  lib.symbols.zclient_free(this.ptr);
655
655
  this.ptr = null;
656
656
  }
657
657
  }
658
658
 
659
+ /**
660
+ * Dispose method for the `using` keyword (ECMAScript Explicit Resource Management)
661
+ * @example
662
+ * ```typescript
663
+ * using client = new JirenClient({ targets: [...] });
664
+ * // client is automatically closed when the scope ends
665
+ * ```
666
+ */
667
+ [Symbol.dispose](): void {
668
+ this.close();
669
+ }
670
+
659
671
  /**
660
672
  * Register interceptors dynamically.
661
673
  * @param interceptors - Interceptor configuration to add
@@ -1111,9 +1123,19 @@ export class JirenClient<
1111
1123
  if (bodyLen > 0 && bodyPtr) {
1112
1124
  buffer = toArrayBuffer(bodyPtr, 0, bodyLen).slice(0);
1113
1125
 
1114
- // Handle GZIP decompression
1126
+ // Handle GZIP decompression - check content-encoding header first
1127
+ const contentEncoding =
1128
+ headersObj instanceof NativeHeaders
1129
+ ? headersObj.get("content-encoding")?.toLowerCase()
1130
+ : (headersObj as Record<string, string>)[
1131
+ "content-encoding"
1132
+ ]?.toLowerCase();
1133
+
1115
1134
  const bufferView = new Uint8Array(buffer);
1135
+
1136
+ // Only attempt gzip if content-encoding is gzip AND magic bytes match
1116
1137
  if (
1138
+ contentEncoding === "gzip" &&
1117
1139
  bufferView.length >= 2 &&
1118
1140
  bufferView[0] === 0x1f &&
1119
1141
  bufferView[1] === 0x8b
@@ -1124,8 +1146,8 @@ export class JirenClient<
1124
1146
  decompressed.byteOffset,
1125
1147
  decompressed.byteOffset + decompressed.byteLength
1126
1148
  );
1127
- } catch (e) {
1128
- console.warn("[Jiren] gzip decompression failed:", e);
1149
+ } catch {
1150
+ // Silently ignore decompression failures - data may already be decompressed
1129
1151
  }
1130
1152
  }
1131
1153
  }
@@ -1,7 +1,4 @@
1
- /**
2
- * Metrics & Observability System for Jiren HTTP Client
3
- * Tracks performance, cache efficiency, errors, and request statistics
4
- */
1
+ // Metrics & Observability System
5
2
 
6
3
  export interface EndpointMetrics {
7
4
  endpoint: string;
@@ -1,5 +1,6 @@
1
1
  import koffi from "koffi";
2
2
  import path from "path";
3
+ import fs from "fs";
3
4
  import { fileURLToPath } from "url";
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
@@ -14,9 +15,12 @@ if (platform === "darwin") {
14
15
  suffix = "dll";
15
16
  }
16
17
 
17
- // Resolve library path relative to this module
18
- // native.ts uses join(import.meta.dir, `../lib/libhttpclient.${suffix}`)
19
- const libPath = path.join(__dirname, `../lib/libhttpclient.${suffix}`);
18
+ // Resolve library path (source or dist)
19
+ let libPath = path.join(__dirname, `../lib/libhttpclient.${suffix}`);
20
+ if (!fs.existsSync(libPath)) {
21
+ // Try two levels up (for dist/components/ -> lib/)
22
+ libPath = path.join(__dirname, `../../lib/libhttpclient.${suffix}`);
23
+ }
20
24
 
21
25
  // Load the library
22
26
  // koffi.load returns an object where we can register functions
@@ -124,6 +124,35 @@ export const ffiDef = {
124
124
  returns: FFIType.u64,
125
125
  },
126
126
 
127
+ // =========================================================================
128
+ // MULTIPLEXING API
129
+ // =========================================================================
130
+
131
+ zclient_request_batch: {
132
+ args: [
133
+ FFIType.ptr, // client
134
+ FFIType.ptr, // requests array (ZBatchRequest*)
135
+ FFIType.u64, // request_count
136
+ ],
137
+ returns: FFIType.ptr, // ZBatchResult*
138
+ },
139
+ zclient_batch_result_free: {
140
+ args: [FFIType.ptr],
141
+ returns: FFIType.void,
142
+ },
143
+ zbatch_response_status: {
144
+ args: [FFIType.ptr, FFIType.u64], // result, index
145
+ returns: FFIType.u16,
146
+ },
147
+ zbatch_response_body: {
148
+ args: [FFIType.ptr, FFIType.u64], // result, index
149
+ returns: FFIType.ptr,
150
+ },
151
+ zbatch_response_body_len: {
152
+ args: [FFIType.ptr, FFIType.u64], // result, index
153
+ returns: FFIType.u64,
154
+ },
155
+
127
156
  // =========================================================================
128
157
  // CACHE FFI
129
158
  // =========================================================================
@@ -0,0 +1,73 @@
1
+ // Persistent subprocess worker - stays alive and processes multiple requests
2
+ import { CString, type Pointer } from "bun:ffi";
3
+ import { lib } from "./native";
4
+
5
+ // Create client immediately
6
+ const ptr: Pointer | null = lib.symbols.zclient_new();
7
+ if (!ptr) {
8
+ console.error("ERROR: Failed to create client");
9
+ process.exit(1);
10
+ }
11
+
12
+ // Enable benchmark mode
13
+ lib.symbols.zclient_set_benchmark_mode(ptr, true);
14
+
15
+ // Read requests from stdin line by line
16
+ const decoder = new TextDecoder();
17
+
18
+ process.stdin.on("data", async (chunk: Buffer) => {
19
+ const lines = decoder.decode(chunk).trim().split("\n");
20
+
21
+ for (const line of lines) {
22
+ if (!line) continue;
23
+
24
+ try {
25
+ const { id, url, method = "GET" } = JSON.parse(line);
26
+
27
+ if (url === "PREFETCH") {
28
+ // Prefetch warmup
29
+ console.log(JSON.stringify({ id, type: "prefetch_done" }));
30
+ continue;
31
+ }
32
+
33
+ if (url === "QUIT") {
34
+ lib.symbols.zclient_free(ptr);
35
+ process.exit(0);
36
+ }
37
+
38
+ const methodBuffer = Buffer.from(method + "\0");
39
+ const urlBuffer = Buffer.from(url + "\0");
40
+
41
+ const respPtr = lib.symbols.zclient_request_full(
42
+ ptr,
43
+ methodBuffer,
44
+ urlBuffer,
45
+ null,
46
+ null,
47
+ 5,
48
+ false
49
+ );
50
+
51
+ if (!respPtr) {
52
+ console.log(
53
+ JSON.stringify({ id, success: false, error: "Request failed" })
54
+ );
55
+ continue;
56
+ }
57
+
58
+ const status = lib.symbols.zfull_response_status(respPtr);
59
+ const bodyLen = Number(lib.symbols.zfull_response_body_len(respPtr));
60
+
61
+ lib.symbols.zclient_response_full_free(respPtr);
62
+
63
+ console.log(JSON.stringify({ id, success: true, status, bodyLen }));
64
+ } catch (err: any) {
65
+ console.log(
66
+ JSON.stringify({ id: 0, success: false, error: err.message })
67
+ );
68
+ }
69
+ }
70
+ });
71
+
72
+ // Signal ready
73
+ console.log(JSON.stringify({ type: "ready" }));
@@ -0,0 +1,65 @@
1
+ // Worker subprocess - runs in isolated process to avoid library conflicts
2
+ import { CString, type Pointer } from "bun:ffi";
3
+ import { lib } from "./native";
4
+
5
+ // Get request from stdin
6
+ const decoder = new TextDecoder();
7
+ let inputBuffer = "";
8
+
9
+ // Create client immediately since we're in a separate process
10
+ const ptr: Pointer | null = lib.symbols.zclient_new();
11
+ if (!ptr) {
12
+ console.error(
13
+ JSON.stringify({ success: false, error: "Failed to create client" })
14
+ );
15
+ process.exit(1);
16
+ }
17
+
18
+ // Enable benchmark mode
19
+ lib.symbols.zclient_set_benchmark_mode(ptr, true);
20
+
21
+ // Read request from argv
22
+ const url = process.argv[2];
23
+ const method = process.argv[3] || "GET";
24
+
25
+ if (!url) {
26
+ console.error(JSON.stringify({ success: false, error: "No URL provided" }));
27
+ process.exit(1);
28
+ }
29
+
30
+ try {
31
+ const methodBuffer = Buffer.from(method + "\0");
32
+ const urlBuffer = Buffer.from(url + "\0");
33
+
34
+ const respPtr = lib.symbols.zclient_request_full(
35
+ ptr,
36
+ methodBuffer,
37
+ urlBuffer,
38
+ null,
39
+ null,
40
+ 5,
41
+ false
42
+ );
43
+
44
+ if (!respPtr) throw new Error("Native request failed");
45
+
46
+ const status = lib.symbols.zfull_response_status(respPtr);
47
+ const bodyLen = Number(lib.symbols.zfull_response_body_len(respPtr));
48
+ const bodyPtr = lib.symbols.zfull_response_body(respPtr);
49
+
50
+ let bodyString = "";
51
+ if (bodyLen > 0 && bodyPtr) {
52
+ bodyString = new CString(bodyPtr).toString();
53
+ }
54
+
55
+ lib.symbols.zclient_response_full_free(respPtr);
56
+ lib.symbols.zclient_free(ptr);
57
+
58
+ console.log(
59
+ JSON.stringify({ success: true, status, bodyLen: bodyString.length })
60
+ );
61
+ } catch (err: any) {
62
+ console.log(JSON.stringify({ success: false, error: err.message }));
63
+ lib.symbols.zclient_free(ptr);
64
+ process.exit(1);
65
+ }
@@ -0,0 +1,169 @@
1
+ import { Worker } from "worker_threads";
2
+ import { join } from "path";
3
+ import type { RequestOptions } from "./types";
4
+
5
+ interface WorkerTask {
6
+ id: number;
7
+ url: string;
8
+ options: RequestOptions;
9
+ resolve: (value: any) => void;
10
+ reject: (error: any) => void;
11
+ }
12
+
13
+ interface WorkerWrapper {
14
+ worker: Worker;
15
+ busy: boolean;
16
+ taskQueue: WorkerTask[];
17
+ }
18
+
19
+ /**
20
+ * WorkerPool manages a pool of worker threads for parallel HTTP requests.
21
+ * Each worker has its own native client instance, allowing true parallel execution.
22
+ */
23
+ export class WorkerPool {
24
+ private workers: WorkerWrapper[] = [];
25
+ private taskId = 0;
26
+ private pendingTasks = new Map<number, WorkerTask>();
27
+ private ready: Promise<void>;
28
+ private closed = false;
29
+
30
+ constructor(poolSize: number = 4) {
31
+ const workerPath = join(import.meta.dir, "worker.ts");
32
+
33
+ const readyPromises: Promise<void>[] = [];
34
+
35
+ for (let i = 0; i < poolSize; i++) {
36
+ const worker = new Worker(workerPath);
37
+ const wrapper: WorkerWrapper = {
38
+ worker,
39
+ busy: false,
40
+ taskQueue: [],
41
+ };
42
+
43
+ worker.on("message", (msg: any) => {
44
+ if (msg.type === "prefetch_done") {
45
+ return;
46
+ }
47
+
48
+ const task = this.pendingTasks.get(msg.id);
49
+ if (task) {
50
+ this.pendingTasks.delete(msg.id);
51
+ wrapper.busy = false;
52
+
53
+ if (msg.success) {
54
+ task.resolve(msg.response);
55
+ } else {
56
+ task.reject(new Error(msg.error));
57
+ }
58
+
59
+ // Process next task in queue
60
+ this.processQueue(wrapper);
61
+ }
62
+ });
63
+
64
+ worker.on("error", (err) => {
65
+ console.error("Worker error:", err);
66
+ });
67
+
68
+ this.workers.push(wrapper);
69
+ }
70
+
71
+ this.ready = Promise.resolve();
72
+ }
73
+
74
+ /**
75
+ * Wait for the pool to be ready
76
+ */
77
+ async waitReady(): Promise<void> {
78
+ return this.ready;
79
+ }
80
+
81
+ /**
82
+ * Submit a request to the worker pool
83
+ */
84
+ async request(url: string, options: RequestOptions = {}): Promise<any> {
85
+ if (this.closed) {
86
+ throw new Error("Worker pool is closed");
87
+ }
88
+
89
+ return new Promise((resolve, reject) => {
90
+ const task: WorkerTask = {
91
+ id: this.taskId++,
92
+ url,
93
+ options,
94
+ resolve,
95
+ reject,
96
+ };
97
+
98
+ // Find the least busy worker
99
+ if (this.workers.length === 0) {
100
+ throw new Error("Worker pool has no workers");
101
+ }
102
+ let leastBusy = this.workers[0]!;
103
+ for (const w of this.workers) {
104
+ if (!w.busy && w.taskQueue.length === 0) {
105
+ leastBusy = w;
106
+ break;
107
+ }
108
+ if (w.taskQueue.length < leastBusy.taskQueue.length) {
109
+ leastBusy = w;
110
+ }
111
+ }
112
+
113
+ leastBusy.taskQueue.push(task);
114
+ this.processQueue(leastBusy);
115
+ });
116
+ }
117
+
118
+ private processQueue(wrapper: WorkerWrapper): void {
119
+ if (wrapper.busy || wrapper.taskQueue.length === 0) {
120
+ return;
121
+ }
122
+
123
+ const task = wrapper.taskQueue.shift()!;
124
+ wrapper.busy = true;
125
+ this.pendingTasks.set(task.id, task);
126
+
127
+ wrapper.worker.postMessage({
128
+ id: task.id,
129
+ url: task.url,
130
+ options: task.options,
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Pre-warm connections for a list of URLs across all workers
136
+ */
137
+ async prefetch(urls: string[]): Promise<void> {
138
+ const promises = this.workers.map(
139
+ (w) =>
140
+ new Promise<void>((resolve) => {
141
+ const handler = (msg: any) => {
142
+ if (msg.type === "prefetch_done") {
143
+ w.worker.off("message", handler);
144
+ resolve();
145
+ }
146
+ };
147
+ w.worker.on("message", handler);
148
+ w.worker.postMessage({ type: "prefetch", url: urls });
149
+ })
150
+ );
151
+ await Promise.all(promises);
152
+ }
153
+
154
+ /**
155
+ * Close all workers
156
+ */
157
+ async close(): Promise<void> {
158
+ this.closed = true;
159
+ const closePromises = this.workers.map(
160
+ (w) =>
161
+ new Promise<void>((resolve) => {
162
+ w.worker.once("exit", () => resolve());
163
+ w.worker.postMessage({ type: "close" });
164
+ })
165
+ );
166
+ await Promise.all(closePromises);
167
+ this.workers = [];
168
+ }
169
+ }
@@ -1,13 +1,22 @@
1
1
  import { parentPort } from "worker_threads";
2
2
  import { CString, type Pointer } from "bun:ffi";
3
3
  import { lib } from "./native";
4
- import type { RequestOptions } from "../types";
4
+ import type { RequestOptions } from "./types";
5
5
 
6
- // Each worker has its own isolated native client
7
- let ptr: Pointer | null = lib.symbols.zclient_new();
6
+ // LAZY INITIALIZATION: Don't create client until first request
7
+ // This avoids race conditions with library loading
8
+ let ptr: Pointer | null = null;
8
9
 
9
- if (!ptr) {
10
- throw new Error("Failed to create native client instance in worker");
10
+ function ensureClient(): Pointer {
11
+ if (!ptr) {
12
+ ptr = lib.symbols.zclient_new();
13
+ if (!ptr) {
14
+ throw new Error("Failed to create native client instance in worker");
15
+ }
16
+ // Enable benchmark mode for consistent performance
17
+ lib.symbols.zclient_set_benchmark_mode(ptr, true);
18
+ }
19
+ return ptr;
11
20
  }
12
21
 
13
22
  // Handle cleanup
@@ -37,20 +46,27 @@ if (parentPort) {
37
46
  }
38
47
 
39
48
  if (msg.type === "prefetch") {
40
- if (!ptr) return;
41
- const urls = msg.url as unknown as string[]; // hack: url field used for string[]
42
- for (const url of urls) {
43
- const urlBuffer = Buffer.from(url + "\0");
44
- lib.symbols.zclient_prefetch(ptr, urlBuffer);
49
+ try {
50
+ const client = ensureClient();
51
+ const urls = msg.url as unknown as string[];
52
+ for (const url of urls) {
53
+ const urlBuffer = Buffer.from(url + "\0");
54
+ lib.symbols.zclient_prefetch(client, urlBuffer);
55
+ }
56
+ parentPort?.postMessage({ type: "prefetch_done" });
57
+ } catch (err: any) {
58
+ parentPort?.postMessage({
59
+ type: "prefetch_done",
60
+ error: err.message,
61
+ });
45
62
  }
46
- parentPort?.postMessage({ type: "prefetch_done" });
47
63
  return;
48
64
  }
49
65
 
50
66
  const { id, url, options } = msg;
51
67
 
52
68
  try {
53
- if (!ptr) throw new Error("Worker client is closed");
69
+ const client = ensureClient();
54
70
 
55
71
  const method = options.method || "GET";
56
72
  const methodBuffer = Buffer.from(method + "\0");
@@ -78,8 +94,9 @@ if (parentPort) {
78
94
  const maxRedirects = options.maxRedirects ?? 5;
79
95
  const antibot = options.antibot ?? false;
80
96
 
81
- const respPtr = lib.symbols.zclient_request(
82
- ptr,
97
+ // Use the full request API for complete response
98
+ const respPtr = lib.symbols.zclient_request_full(
99
+ client,
83
100
  methodBuffer,
84
101
  urlBuffer,
85
102
  headersBuffer,
@@ -88,16 +105,15 @@ if (parentPort) {
88
105
  antibot
89
106
  );
90
107
 
91
- // Parse Response
92
108
  if (!respPtr) throw new Error("Native request failed");
93
109
 
94
- const status = lib.symbols.zclient_response_status(respPtr);
95
- const bodyLen = Number(lib.symbols.zclient_response_body_len(respPtr));
96
- const bodyPtr = lib.symbols.zclient_response_body(respPtr);
110
+ const status = lib.symbols.zfull_response_status(respPtr);
111
+ const bodyLen = Number(lib.symbols.zfull_response_body_len(respPtr));
112
+ const bodyPtr = lib.symbols.zfull_response_body(respPtr);
97
113
  const headersLen = Number(
98
- lib.symbols.zclient_response_headers_len(respPtr)
114
+ lib.symbols.zfull_response_headers_len(respPtr)
99
115
  );
100
- const headersPtr = lib.symbols.zclient_response_headers(respPtr);
116
+ const headersRawPtr = lib.symbols.zfull_response_headers(respPtr);
101
117
 
102
118
  let bodyString = "";
103
119
  if (bodyLen > 0 && bodyPtr) {
@@ -105,8 +121,8 @@ if (parentPort) {
105
121
  }
106
122
 
107
123
  const headers: Record<string, string> = {};
108
- if (headersLen > 0 && headersPtr) {
109
- const headersStr = new CString(headersPtr).toString();
124
+ if (headersLen > 0 && headersRawPtr) {
125
+ const headersStr = new CString(headersRawPtr).toString();
110
126
  const lines = headersStr.split("\r\n");
111
127
  for (const line of lines) {
112
128
  if (!line) continue;
@@ -119,7 +135,7 @@ if (parentPort) {
119
135
  }
120
136
  }
121
137
 
122
- lib.symbols.zclient_response_free(respPtr);
138
+ lib.symbols.zclient_response_full_free(respPtr);
123
139
 
124
140
  parentPort?.postMessage({
125
141
  id,