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.
- package/README.md +321 -483
- package/components/cache.ts +1 -1
- package/components/client-node-native.ts +497 -159
- package/components/client.ts +51 -29
- package/components/metrics.ts +1 -4
- package/components/native-node.ts +7 -3
- package/components/native.ts +29 -0
- package/components/persistent-worker.ts +73 -0
- package/components/subprocess-worker.ts +65 -0
- package/components/worker-pool.ts +169 -0
- package/components/worker.ts +39 -23
- package/dist/components/cache.d.ts +76 -0
- package/dist/components/cache.d.ts.map +1 -0
- package/dist/components/cache.js +439 -0
- package/dist/components/cache.js.map +1 -0
- package/dist/components/client-node-native.d.ts +114 -0
- package/dist/components/client-node-native.d.ts.map +1 -0
- package/dist/components/client-node-native.js +744 -0
- package/dist/components/client-node-native.js.map +1 -0
- package/dist/components/metrics.d.ts +104 -0
- package/dist/components/metrics.d.ts.map +1 -0
- package/dist/components/metrics.js +296 -0
- package/dist/components/metrics.js.map +1 -0
- package/dist/components/native-node.d.ts +60 -0
- package/dist/components/native-node.d.ts.map +1 -0
- package/dist/components/native-node.js +108 -0
- package/dist/components/native-node.js.map +1 -0
- package/dist/components/types.d.ts +250 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +5 -0
- package/dist/components/types.js.map +1 -0
- package/dist/index-node.d.ts +10 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index-node.js +12 -0
- package/dist/index-node.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/index-node.ts +6 -6
- package/index.ts +4 -3
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +13 -5
- package/types/index.ts +0 -68
package/components/client.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface JirenClientOptions<
|
|
|
62
62
|
performanceMode?: boolean;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
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
|
|
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
|
|
120
|
+
// Pre-computed headers
|
|
127
121
|
private readonly defaultHeadersStr: string;
|
|
128
|
-
private readonly defaultHeadersBuffer: 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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
|
1128
|
-
|
|
1149
|
+
} catch {
|
|
1150
|
+
// Silently ignore decompression failures - data may already be decompressed
|
|
1129
1151
|
}
|
|
1130
1152
|
}
|
|
1131
1153
|
}
|
package/components/metrics.ts
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
package/components/native.ts
CHANGED
|
@@ -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
|
+
}
|
package/components/worker.ts
CHANGED
|
@@ -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 "
|
|
4
|
+
import type { RequestOptions } from "./types";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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.
|
|
95
|
-
const bodyLen = Number(lib.symbols.
|
|
96
|
-
const bodyPtr = lib.symbols.
|
|
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.
|
|
114
|
+
lib.symbols.zfull_response_headers_len(respPtr)
|
|
99
115
|
);
|
|
100
|
-
const
|
|
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 &&
|
|
109
|
-
const headersStr = new CString(
|
|
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.
|
|
138
|
+
lib.symbols.zclient_response_full_free(respPtr);
|
|
123
139
|
|
|
124
140
|
parentPort?.postMessage({
|
|
125
141
|
id,
|