jiren 1.5.5 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -313
- package/components/cache.ts +1 -1
- package/components/client-node-native.ts +602 -159
- package/components/client.ts +51 -29
- package/components/metrics.ts +1 -4
- package/components/native-node.ts +48 -3
- package/components/native.ts +29 -0
- package/components/persistent-worker.ts +73 -0
- package/components/subprocess-worker.ts +65 -0
- package/components/types.ts +2 -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 +134 -0
- package/dist/components/client-node-native.d.ts.map +1 -0
- package/dist/components/client-node-native.js +811 -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 +67 -0
- package/dist/components/native-node.d.ts.map +1 -0
- package/dist/components/native-node.js +137 -0
- package/dist/components/native-node.js.map +1 -0
- package/dist/components/types.d.ts +252 -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 +15 -8
- package/types/index.ts +0 -68
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
nativeLib as lib,
|
|
3
|
+
ZPipelineRequest,
|
|
4
|
+
ZPipelineResponse,
|
|
5
|
+
ZPipelineResult,
|
|
6
|
+
} from "./native-node.js";
|
|
7
|
+
import { ResponseCache } from "./cache.js";
|
|
8
|
+
import { MetricsCollector, type RequestMetric } from "./metrics.js";
|
|
3
9
|
import koffi from "koffi";
|
|
4
10
|
import zlib from "zlib";
|
|
5
11
|
import type {
|
|
@@ -10,7 +16,15 @@ import type {
|
|
|
10
16
|
UrlRequestOptions,
|
|
11
17
|
UrlEndpoint,
|
|
12
18
|
CacheConfig,
|
|
13
|
-
|
|
19
|
+
RetryConfig,
|
|
20
|
+
Interceptors,
|
|
21
|
+
RequestInterceptor,
|
|
22
|
+
ResponseInterceptor,
|
|
23
|
+
ErrorInterceptor,
|
|
24
|
+
InterceptorRequestContext,
|
|
25
|
+
InterceptorResponseContext,
|
|
26
|
+
MetricsAPI,
|
|
27
|
+
} from "./types.js";
|
|
14
28
|
|
|
15
29
|
const STATUS_TEXT: Record<number, string> = {
|
|
16
30
|
200: "OK",
|
|
@@ -27,8 +41,10 @@ const STATUS_TEXT: Record<number, string> = {
|
|
|
27
41
|
503: "Service Unavailable",
|
|
28
42
|
};
|
|
29
43
|
|
|
30
|
-
/** URL configuration with optional cache */
|
|
31
|
-
export type UrlConfig =
|
|
44
|
+
/** URL configuration with optional cache and antibot */
|
|
45
|
+
export type UrlConfig =
|
|
46
|
+
| string
|
|
47
|
+
| { url: string; cache?: boolean | CacheConfig; antibot?: boolean };
|
|
32
48
|
|
|
33
49
|
/** Options for JirenClient constructor */
|
|
34
50
|
export interface JirenClientOptions<
|
|
@@ -36,15 +52,27 @@ export interface JirenClientOptions<
|
|
|
36
52
|
| readonly TargetUrlConfig[]
|
|
37
53
|
| Record<string, UrlConfig>
|
|
38
54
|
> {
|
|
39
|
-
/** URLs to
|
|
40
|
-
|
|
55
|
+
/** Target URLs to pre-connect on client creation */
|
|
56
|
+
targets?: string[] | T;
|
|
41
57
|
|
|
42
58
|
/** Enable benchmark mode (Force HTTP/2, disable probing) */
|
|
43
59
|
benchmark?: boolean;
|
|
60
|
+
|
|
61
|
+
/** Global retry configuration */
|
|
62
|
+
retry?: number | RetryConfig;
|
|
63
|
+
|
|
64
|
+
/** Request/response interceptors */
|
|
65
|
+
interceptors?: Interceptors;
|
|
66
|
+
|
|
67
|
+
/** Performance mode: disable metrics, skip cache checks for non-cached endpoints (default: true) */
|
|
68
|
+
performanceMode?: boolean;
|
|
69
|
+
|
|
70
|
+
/** Enable default browser emulation headers (default: true) */
|
|
71
|
+
defaultHeaders?: boolean;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
export type
|
|
74
|
+
// Helper to extract keys from Target Config
|
|
75
|
+
export type ExtractTargetKeys<
|
|
48
76
|
T extends readonly TargetUrlConfig[] | Record<string, UrlConfig>
|
|
49
77
|
> = T extends readonly TargetUrlConfig[]
|
|
50
78
|
? T[number]["key"]
|
|
@@ -52,15 +80,14 @@ export type ExtractWarmupKeys<
|
|
|
52
80
|
? keyof T
|
|
53
81
|
: never;
|
|
54
82
|
|
|
55
|
-
/** Type-safe URL accessor - maps keys to UrlEndpoint objects */
|
|
56
83
|
export type UrlAccessor<
|
|
57
84
|
T extends readonly TargetUrlConfig[] | Record<string, UrlConfig>
|
|
58
85
|
> = {
|
|
59
|
-
[K in
|
|
86
|
+
[K in ExtractTargetKeys<T>]: UrlEndpoint;
|
|
60
87
|
};
|
|
61
88
|
|
|
62
89
|
/**
|
|
63
|
-
* Helper
|
|
90
|
+
* Helper to define target URLs with type inference.
|
|
64
91
|
*/
|
|
65
92
|
export function defineUrls<const T extends readonly TargetUrlConfig[]>(
|
|
66
93
|
urls: T
|
|
@@ -68,49 +95,122 @@ export function defineUrls<const T extends readonly TargetUrlConfig[]>(
|
|
|
68
95
|
return urls;
|
|
69
96
|
}
|
|
70
97
|
|
|
98
|
+
// Cleanup native resources on GC
|
|
99
|
+
const clientRegistry = new FinalizationRegistry<any>((ptr) => {
|
|
100
|
+
try {
|
|
101
|
+
lib.symbols.zclient_free(ptr);
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore cleanup errors
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
71
107
|
export class JirenClient<
|
|
72
108
|
T extends readonly TargetUrlConfig[] | Record<string, UrlConfig> =
|
|
73
109
|
| readonly TargetUrlConfig[]
|
|
74
110
|
| Record<string, UrlConfig>
|
|
75
|
-
>
|
|
76
|
-
|
|
111
|
+
> implements Disposable
|
|
112
|
+
{
|
|
113
|
+
private ptr: any = null; // Koffi pointer
|
|
77
114
|
private urlMap: Map<string, string> = new Map();
|
|
78
115
|
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
79
116
|
new Map();
|
|
80
117
|
private antibotConfig: Map<string, boolean> = new Map();
|
|
81
118
|
private cache: ResponseCache;
|
|
119
|
+
private inflightRequests: Map<string, Promise<any>> = new Map();
|
|
120
|
+
private globalRetry?: RetryConfig;
|
|
121
|
+
private requestInterceptors: RequestInterceptor[] = [];
|
|
122
|
+
private responseInterceptors: ResponseInterceptor[] = [];
|
|
123
|
+
private errorInterceptors: ErrorInterceptor[] = [];
|
|
124
|
+
private targetsPromise: Promise<void> | null = null;
|
|
125
|
+
private targetsComplete: Set<string> = new Set();
|
|
126
|
+
private performanceMode: boolean = false;
|
|
127
|
+
private useDefaultHeaders: boolean = true;
|
|
128
|
+
|
|
129
|
+
// Pre-computed headers
|
|
130
|
+
private readonly defaultHeadersStr: string;
|
|
131
|
+
private readonly defaultHeaders: Record<string, string> = {
|
|
132
|
+
"user-agent":
|
|
133
|
+
"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",
|
|
134
|
+
accept:
|
|
135
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
136
|
+
"accept-encoding": "gzip",
|
|
137
|
+
"accept-language": "en-US,en;q=0.9",
|
|
138
|
+
"sec-ch-ua":
|
|
139
|
+
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
140
|
+
"sec-ch-ua-mobile": "?0",
|
|
141
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
142
|
+
"sec-fetch-dest": "document",
|
|
143
|
+
"sec-fetch-mode": "navigate",
|
|
144
|
+
"sec-fetch-site": "none",
|
|
145
|
+
"sec-fetch-user": "?1",
|
|
146
|
+
"upgrade-insecure-requests": "1",
|
|
147
|
+
};
|
|
82
148
|
|
|
83
149
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
84
150
|
public readonly url: UrlAccessor<T>;
|
|
85
151
|
|
|
152
|
+
// Metrics collector
|
|
153
|
+
private metricsCollector: MetricsCollector;
|
|
154
|
+
/** Public metrics API */
|
|
155
|
+
public readonly metrics: MetricsAPI;
|
|
156
|
+
|
|
86
157
|
constructor(options?: JirenClientOptions<T>) {
|
|
87
158
|
this.ptr = lib.symbols.zclient_new();
|
|
88
159
|
if (!this.ptr) throw new Error("Failed to create native client instance");
|
|
89
160
|
|
|
161
|
+
clientRegistry.register(this, this.ptr, this);
|
|
162
|
+
|
|
163
|
+
// Pre-computed default headers string
|
|
164
|
+
const orderedKeys = [
|
|
165
|
+
"sec-ch-ua",
|
|
166
|
+
"sec-ch-ua-mobile",
|
|
167
|
+
"sec-ch-ua-platform",
|
|
168
|
+
"upgrade-insecure-requests",
|
|
169
|
+
"user-agent",
|
|
170
|
+
"accept",
|
|
171
|
+
"sec-fetch-site",
|
|
172
|
+
"sec-fetch-mode",
|
|
173
|
+
"sec-fetch-user",
|
|
174
|
+
"sec-fetch-dest",
|
|
175
|
+
"accept-encoding",
|
|
176
|
+
"accept-language",
|
|
177
|
+
];
|
|
178
|
+
this.defaultHeadersStr = orderedKeys
|
|
179
|
+
.map((k) => `${k}: ${this.defaultHeaders[k]}`)
|
|
180
|
+
.join("\r\n");
|
|
181
|
+
|
|
90
182
|
// Initialize cache
|
|
91
183
|
this.cache = new ResponseCache(100);
|
|
92
184
|
|
|
185
|
+
// Initialize metrics
|
|
186
|
+
this.metricsCollector = new MetricsCollector();
|
|
187
|
+
this.metrics = this.metricsCollector;
|
|
188
|
+
|
|
189
|
+
// Performance mode (default: true for maximum speed)
|
|
190
|
+
this.performanceMode = options?.performanceMode ?? true;
|
|
191
|
+
|
|
192
|
+
// Default headers (default: true)
|
|
193
|
+
this.useDefaultHeaders = options?.defaultHeaders ?? true;
|
|
194
|
+
|
|
93
195
|
// Enable benchmark mode if requested
|
|
94
196
|
if (options?.benchmark) {
|
|
95
197
|
lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
|
|
96
198
|
}
|
|
97
199
|
|
|
98
|
-
// Process
|
|
99
|
-
if (options?.
|
|
200
|
+
// Process target URLs
|
|
201
|
+
if (options?.targets) {
|
|
100
202
|
const urls: string[] = [];
|
|
101
|
-
const
|
|
203
|
+
const targets = options.targets;
|
|
102
204
|
|
|
103
|
-
if (Array.isArray(
|
|
104
|
-
for (const item of
|
|
205
|
+
if (Array.isArray(targets)) {
|
|
206
|
+
for (const item of targets) {
|
|
105
207
|
if (typeof item === "string") {
|
|
106
208
|
urls.push(item);
|
|
107
209
|
} else {
|
|
108
|
-
// TargetUrlConfig with key and optional cache
|
|
109
210
|
const config = item as TargetUrlConfig;
|
|
110
211
|
urls.push(config.url);
|
|
111
212
|
this.urlMap.set(config.key, config.url);
|
|
112
213
|
|
|
113
|
-
// Store cache config
|
|
114
214
|
if (config.cache) {
|
|
115
215
|
const cacheConfig =
|
|
116
216
|
typeof config.cache === "boolean"
|
|
@@ -119,25 +219,23 @@ export class JirenClient<
|
|
|
119
219
|
this.cacheConfig.set(config.key, cacheConfig);
|
|
120
220
|
}
|
|
121
221
|
|
|
122
|
-
// Store antibot config
|
|
123
222
|
if (config.antibot) {
|
|
124
223
|
this.antibotConfig.set(config.key, true);
|
|
125
224
|
}
|
|
126
225
|
}
|
|
127
226
|
}
|
|
128
227
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
228
|
+
for (const [key, urlConfig] of Object.entries(targets) as [
|
|
229
|
+
string,
|
|
230
|
+
UrlConfig
|
|
231
|
+
][]) {
|
|
131
232
|
if (typeof urlConfig === "string") {
|
|
132
|
-
// Simple string URL
|
|
133
233
|
urls.push(urlConfig);
|
|
134
234
|
this.urlMap.set(key, urlConfig);
|
|
135
235
|
} else {
|
|
136
|
-
// URL config object with cache
|
|
137
236
|
urls.push(urlConfig.url);
|
|
138
237
|
this.urlMap.set(key, urlConfig.url);
|
|
139
238
|
|
|
140
|
-
// Store cache config
|
|
141
239
|
if (urlConfig.cache) {
|
|
142
240
|
const cacheConfig =
|
|
143
241
|
typeof urlConfig.cache === "boolean"
|
|
@@ -146,8 +244,7 @@ export class JirenClient<
|
|
|
146
244
|
this.cacheConfig.set(key, cacheConfig);
|
|
147
245
|
}
|
|
148
246
|
|
|
149
|
-
|
|
150
|
-
if ((urlConfig as { antibot?: boolean }).antibot) {
|
|
247
|
+
if (urlConfig.antibot) {
|
|
151
248
|
this.antibotConfig.set(key, true);
|
|
152
249
|
}
|
|
153
250
|
}
|
|
@@ -155,11 +252,13 @@ export class JirenClient<
|
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
if (urls.length > 0) {
|
|
158
|
-
this.
|
|
255
|
+
this.targetsPromise = this.preconnect(urls).then(() => {
|
|
256
|
+
urls.forEach((url) => this.targetsComplete.add(url));
|
|
257
|
+
this.targetsPromise = null;
|
|
258
|
+
});
|
|
159
259
|
}
|
|
160
260
|
|
|
161
|
-
// Preload L2 disk cache entries into L1 memory
|
|
162
|
-
// This ensures user's first request hits L1 (~0.001ms) instead of L2 (~5ms)
|
|
261
|
+
// Preload L2 disk cache entries into L1 memory
|
|
163
262
|
for (const [key, config] of this.cacheConfig.entries()) {
|
|
164
263
|
if (config.enabled) {
|
|
165
264
|
const url = this.urlMap.get(key);
|
|
@@ -172,13 +271,39 @@ export class JirenClient<
|
|
|
172
271
|
|
|
173
272
|
// Create proxy for type-safe URL access
|
|
174
273
|
this.url = this.createUrlAccessor();
|
|
274
|
+
|
|
275
|
+
// Store global retry config
|
|
276
|
+
if (options?.retry) {
|
|
277
|
+
this.globalRetry =
|
|
278
|
+
typeof options.retry === "number"
|
|
279
|
+
? { count: options.retry, delay: 100, backoff: 2 }
|
|
280
|
+
: options.retry;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Initialize interceptors
|
|
284
|
+
if (options?.interceptors) {
|
|
285
|
+
this.requestInterceptors = options.interceptors.request || [];
|
|
286
|
+
this.responseInterceptors = options.interceptors.response || [];
|
|
287
|
+
this.errorInterceptors = options.interceptors.error || [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async waitFor(ms: number) {
|
|
292
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
175
293
|
}
|
|
176
294
|
|
|
177
295
|
/**
|
|
178
|
-
* Wait for
|
|
296
|
+
* Wait for lazy pre-connection to complete.
|
|
297
|
+
*/
|
|
298
|
+
public async waitForTargets(): Promise<void> {
|
|
299
|
+
if (this.targetsPromise) await this.targetsPromise;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @deprecated Use waitForTargets() instead
|
|
179
304
|
*/
|
|
180
305
|
public async waitForWarmup(): Promise<void> {
|
|
181
|
-
|
|
306
|
+
return this.waitForTargets();
|
|
182
307
|
}
|
|
183
308
|
|
|
184
309
|
/**
|
|
@@ -207,58 +332,173 @@ export class JirenClient<
|
|
|
207
332
|
get: async <R = any>(
|
|
208
333
|
options?: UrlRequestOptions
|
|
209
334
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
210
|
-
|
|
335
|
+
if (self.targetsPromise) {
|
|
336
|
+
await self.targetsPromise;
|
|
337
|
+
}
|
|
211
338
|
|
|
212
|
-
|
|
339
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
213
340
|
const useAntibot =
|
|
214
341
|
options?.antibot ?? self.antibotConfig.get(prop) ?? false;
|
|
215
342
|
|
|
343
|
+
// Fast path: no cache
|
|
344
|
+
if (!cacheConfig?.enabled && self.performanceMode) {
|
|
345
|
+
return self.request<R>("GET", buildUrl(options?.path), null, {
|
|
346
|
+
headers: options?.headers,
|
|
347
|
+
maxRedirects: options?.maxRedirects,
|
|
348
|
+
responseType: options?.responseType,
|
|
349
|
+
antibot: useAntibot,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const startTime = performance.now();
|
|
354
|
+
|
|
355
|
+
// Try cache
|
|
216
356
|
if (cacheConfig?.enabled) {
|
|
217
357
|
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
218
358
|
if (cached) {
|
|
359
|
+
const responseTimeMs = performance.now() - startTime;
|
|
360
|
+
const cacheLayer = responseTimeMs < 0.5 ? "l1" : "l2";
|
|
361
|
+
|
|
362
|
+
if (!self.performanceMode) {
|
|
363
|
+
self.metricsCollector.recordRequest(prop, {
|
|
364
|
+
startTime,
|
|
365
|
+
responseTimeMs,
|
|
366
|
+
status: cached.status,
|
|
367
|
+
success: cached.ok,
|
|
368
|
+
bytesSent: 0,
|
|
369
|
+
bytesReceived: 0,
|
|
370
|
+
cacheHit: true,
|
|
371
|
+
cacheLayer,
|
|
372
|
+
dedupeHit: false,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
219
376
|
return cached as any;
|
|
220
377
|
}
|
|
221
378
|
}
|
|
222
379
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
380
|
+
// Deduplication
|
|
381
|
+
const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(
|
|
382
|
+
options?.headers || {}
|
|
383
|
+
)}`;
|
|
384
|
+
|
|
385
|
+
if (self.inflightRequests.has(dedupKey)) {
|
|
386
|
+
const dedupeStart = performance.now();
|
|
387
|
+
const result = await self.inflightRequests.get(dedupKey);
|
|
388
|
+
const responseTimeMs = performance.now() - dedupeStart;
|
|
389
|
+
|
|
390
|
+
if (!self.performanceMode) {
|
|
391
|
+
self.metricsCollector.recordRequest(prop, {
|
|
392
|
+
startTime: dedupeStart,
|
|
393
|
+
responseTimeMs,
|
|
394
|
+
status:
|
|
395
|
+
typeof result === "object" && "status" in result
|
|
396
|
+
? result.status
|
|
397
|
+
: 200,
|
|
398
|
+
success: true,
|
|
399
|
+
bytesSent: 0,
|
|
400
|
+
bytesReceived: 0,
|
|
401
|
+
cacheHit: false,
|
|
402
|
+
dedupeHit: true,
|
|
403
|
+
});
|
|
232
404
|
}
|
|
233
|
-
);
|
|
234
405
|
|
|
235
|
-
|
|
236
|
-
cacheConfig?.enabled &&
|
|
237
|
-
typeof response === "object" &&
|
|
238
|
-
"status" in response
|
|
239
|
-
) {
|
|
240
|
-
self.cache.set(
|
|
241
|
-
baseUrl,
|
|
242
|
-
response as JirenResponse,
|
|
243
|
-
cacheConfig.ttl,
|
|
244
|
-
options?.path,
|
|
245
|
-
options
|
|
246
|
-
);
|
|
406
|
+
return result;
|
|
247
407
|
}
|
|
248
408
|
|
|
249
|
-
|
|
409
|
+
const requestPromise = (async () => {
|
|
410
|
+
try {
|
|
411
|
+
const response = await self.request<R>(
|
|
412
|
+
"GET",
|
|
413
|
+
buildUrl(options?.path),
|
|
414
|
+
null,
|
|
415
|
+
{
|
|
416
|
+
headers: options?.headers,
|
|
417
|
+
maxRedirects: options?.maxRedirects,
|
|
418
|
+
responseType: options?.responseType,
|
|
419
|
+
antibot: useAntibot,
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
cacheConfig?.enabled &&
|
|
425
|
+
typeof response === "object" &&
|
|
426
|
+
"status" in response
|
|
427
|
+
) {
|
|
428
|
+
self.cache.set(
|
|
429
|
+
baseUrl,
|
|
430
|
+
response as JirenResponse,
|
|
431
|
+
cacheConfig.ttl,
|
|
432
|
+
options?.path,
|
|
433
|
+
options
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const responseTimeMs = performance.now() - startTime;
|
|
438
|
+
|
|
439
|
+
if (!self.performanceMode) {
|
|
440
|
+
self.metricsCollector.recordRequest(prop, {
|
|
441
|
+
startTime,
|
|
442
|
+
responseTimeMs,
|
|
443
|
+
status:
|
|
444
|
+
typeof response === "object" && "status" in response
|
|
445
|
+
? response.status
|
|
446
|
+
: 200,
|
|
447
|
+
success:
|
|
448
|
+
typeof response === "object" && "ok" in response
|
|
449
|
+
? response.ok
|
|
450
|
+
: true,
|
|
451
|
+
bytesSent: options?.body
|
|
452
|
+
? JSON.stringify(options.body).length
|
|
453
|
+
: 0,
|
|
454
|
+
bytesReceived: 0,
|
|
455
|
+
cacheHit: false,
|
|
456
|
+
dedupeHit: false,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return response;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (!self.performanceMode) {
|
|
463
|
+
const responseTimeMs = performance.now() - startTime;
|
|
464
|
+
self.metricsCollector.recordRequest(prop, {
|
|
465
|
+
startTime,
|
|
466
|
+
responseTimeMs,
|
|
467
|
+
status: 0,
|
|
468
|
+
success: false,
|
|
469
|
+
bytesSent: 0,
|
|
470
|
+
bytesReceived: 0,
|
|
471
|
+
cacheHit: false,
|
|
472
|
+
dedupeHit: false,
|
|
473
|
+
error:
|
|
474
|
+
error instanceof Error ? error.message : String(error),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
throw error;
|
|
479
|
+
} finally {
|
|
480
|
+
self.inflightRequests.delete(dedupKey);
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
483
|
+
|
|
484
|
+
self.inflightRequests.set(dedupKey, requestPromise);
|
|
485
|
+
|
|
486
|
+
return requestPromise;
|
|
250
487
|
},
|
|
251
488
|
|
|
252
489
|
post: async <R = any>(
|
|
253
|
-
body?: string | null,
|
|
254
490
|
options?: UrlRequestOptions
|
|
255
491
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
492
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
493
|
+
options?.body,
|
|
494
|
+
options?.headers
|
|
495
|
+
);
|
|
256
496
|
return self.request<R>(
|
|
257
497
|
"POST",
|
|
258
498
|
buildUrl(options?.path),
|
|
259
|
-
|
|
499
|
+
serializedBody,
|
|
260
500
|
{
|
|
261
|
-
headers
|
|
501
|
+
headers,
|
|
262
502
|
maxRedirects: options?.maxRedirects,
|
|
263
503
|
responseType: options?.responseType,
|
|
264
504
|
}
|
|
@@ -266,15 +506,18 @@ export class JirenClient<
|
|
|
266
506
|
},
|
|
267
507
|
|
|
268
508
|
put: async <R = any>(
|
|
269
|
-
body?: string | null,
|
|
270
509
|
options?: UrlRequestOptions
|
|
271
510
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
511
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
512
|
+
options?.body,
|
|
513
|
+
options?.headers
|
|
514
|
+
);
|
|
272
515
|
return self.request<R>(
|
|
273
516
|
"PUT",
|
|
274
517
|
buildUrl(options?.path),
|
|
275
|
-
|
|
518
|
+
serializedBody,
|
|
276
519
|
{
|
|
277
|
-
headers
|
|
520
|
+
headers,
|
|
278
521
|
maxRedirects: options?.maxRedirects,
|
|
279
522
|
responseType: options?.responseType,
|
|
280
523
|
}
|
|
@@ -282,15 +525,18 @@ export class JirenClient<
|
|
|
282
525
|
},
|
|
283
526
|
|
|
284
527
|
patch: async <R = any>(
|
|
285
|
-
body?: string | null,
|
|
286
528
|
options?: UrlRequestOptions
|
|
287
529
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
530
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
531
|
+
options?.body,
|
|
532
|
+
options?.headers
|
|
533
|
+
);
|
|
288
534
|
return self.request<R>(
|
|
289
535
|
"PATCH",
|
|
290
536
|
buildUrl(options?.path),
|
|
291
|
-
|
|
537
|
+
serializedBody,
|
|
292
538
|
{
|
|
293
|
-
headers
|
|
539
|
+
headers,
|
|
294
540
|
maxRedirects: options?.maxRedirects,
|
|
295
541
|
responseType: options?.responseType,
|
|
296
542
|
}
|
|
@@ -298,15 +544,18 @@ export class JirenClient<
|
|
|
298
544
|
},
|
|
299
545
|
|
|
300
546
|
delete: async <R = any>(
|
|
301
|
-
body?: string | null,
|
|
302
547
|
options?: UrlRequestOptions
|
|
303
548
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
549
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
550
|
+
options?.body,
|
|
551
|
+
options?.headers
|
|
552
|
+
);
|
|
304
553
|
return self.request<R>(
|
|
305
554
|
"DELETE",
|
|
306
555
|
buildUrl(options?.path),
|
|
307
|
-
|
|
556
|
+
serializedBody,
|
|
308
557
|
{
|
|
309
|
-
headers
|
|
558
|
+
headers,
|
|
310
559
|
maxRedirects: options?.maxRedirects,
|
|
311
560
|
responseType: options?.responseType,
|
|
312
561
|
}
|
|
@@ -349,24 +598,55 @@ export class JirenClient<
|
|
|
349
598
|
});
|
|
350
599
|
}
|
|
351
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Free the native client resources.
|
|
603
|
+
* Note: This is called automatically when the client is garbage collected,
|
|
604
|
+
* or you can use the `using` keyword for automatic cleanup in a scope.
|
|
605
|
+
*/
|
|
352
606
|
public close(): void {
|
|
353
607
|
if (this.ptr) {
|
|
608
|
+
// Unregister from FinalizationRegistry since we're manually closing
|
|
609
|
+
clientRegistry.unregister(this);
|
|
354
610
|
lib.symbols.zclient_free(this.ptr);
|
|
355
611
|
this.ptr = null;
|
|
356
612
|
}
|
|
357
613
|
}
|
|
358
614
|
|
|
359
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Dispose method for the `using` keyword (ECMAScript Explicit Resource Management)
|
|
617
|
+
* @example
|
|
618
|
+
* ```typescript
|
|
619
|
+
* using client = new JirenClient({ targets: [...] });
|
|
620
|
+
* // client is automatically closed when the scope ends
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
[Symbol.dispose](): void {
|
|
624
|
+
this.close();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Register interceptors dynamically.
|
|
629
|
+
*/
|
|
630
|
+
public use(interceptors: Interceptors): this {
|
|
631
|
+
if (interceptors.request)
|
|
632
|
+
this.requestInterceptors.push(...interceptors.request);
|
|
633
|
+
if (interceptors.response)
|
|
634
|
+
this.responseInterceptors.push(...interceptors.response);
|
|
635
|
+
if (interceptors.error) this.errorInterceptors.push(...interceptors.error);
|
|
636
|
+
return this;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Pre-connect to URLs in parallel.
|
|
641
|
+
*/
|
|
642
|
+
public async preconnect(urls: string[]): Promise<void> {
|
|
360
643
|
if (!this.ptr) throw new Error("Client is closed");
|
|
361
644
|
|
|
362
645
|
await Promise.all(
|
|
363
646
|
urls.map(
|
|
364
647
|
(url) =>
|
|
365
648
|
new Promise<void>((resolve) => {
|
|
366
|
-
|
|
367
|
-
lib.symbols.zclient_prefetch(this.ptr, url); // Koffi handles string auto-conversion if type is 'const char*' but Buffer is safer for null termination?
|
|
368
|
-
// Koffi: "const char *" expects a string or Buffer. String is null-terminated by Koffi.
|
|
369
|
-
// Using generic string.
|
|
649
|
+
lib.symbols.zclient_prefetch(this.ptr, url);
|
|
370
650
|
resolve();
|
|
371
651
|
})
|
|
372
652
|
)
|
|
@@ -374,10 +654,108 @@ export class JirenClient<
|
|
|
374
654
|
}
|
|
375
655
|
|
|
376
656
|
/**
|
|
377
|
-
* @deprecated Use
|
|
657
|
+
* @deprecated Use preconnect() instead
|
|
658
|
+
*/
|
|
659
|
+
public async warmup(urls: string[]): Promise<void> {
|
|
660
|
+
return this.preconnect(urls);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* @deprecated Use preconnect() instead
|
|
378
665
|
*/
|
|
379
666
|
public prefetch(urls: string[]): void {
|
|
380
|
-
this.
|
|
667
|
+
this.preconnect(urls);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Pre-warm a single URL (establishes connection, caches DNS, TLS session)
|
|
672
|
+
* Call this before making requests to eliminate first-request latency.
|
|
673
|
+
*/
|
|
674
|
+
public async prewarm(url: string): Promise<void> {
|
|
675
|
+
return this.preconnect([url]);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Execute multiple requests in a single pipelined batch (100k+ RPS)
|
|
680
|
+
* All requests must go to the same host.
|
|
681
|
+
* @param host - The host (e.g., "localhost")
|
|
682
|
+
* @param port - The port (e.g., 4000)
|
|
683
|
+
* @param requests - Array of { method, path } objects
|
|
684
|
+
* @returns Array of response bodies as strings
|
|
685
|
+
*/
|
|
686
|
+
public batch(
|
|
687
|
+
host: string,
|
|
688
|
+
port: number,
|
|
689
|
+
requests: Array<{ method: string; path: string }>
|
|
690
|
+
): string[] {
|
|
691
|
+
if (!this.ptr) throw new Error("Client is closed");
|
|
692
|
+
if (requests.length === 0) return [];
|
|
693
|
+
|
|
694
|
+
// Encode strings to buffers and create request structs
|
|
695
|
+
const requestStructs: any[] = [];
|
|
696
|
+
const buffers: Buffer[] = []; // Keep references to prevent GC
|
|
697
|
+
|
|
698
|
+
for (const req of requests) {
|
|
699
|
+
const methodBuf = Buffer.from(req.method + "\0");
|
|
700
|
+
const pathBuf = Buffer.from(req.path + "\0");
|
|
701
|
+
buffers.push(methodBuf, pathBuf);
|
|
702
|
+
|
|
703
|
+
requestStructs.push({
|
|
704
|
+
method_ptr: methodBuf,
|
|
705
|
+
method_len: req.method.length,
|
|
706
|
+
path_ptr: pathBuf,
|
|
707
|
+
path_len: req.path.length,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Create array of structs
|
|
712
|
+
const requestArray = koffi.as(
|
|
713
|
+
requestStructs,
|
|
714
|
+
koffi.pointer(ZPipelineRequest)
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
// Call native pipelined batch
|
|
718
|
+
const resultPtr = lib.symbols.zclient_request_batch_http1(
|
|
719
|
+
this.ptr,
|
|
720
|
+
host,
|
|
721
|
+
port,
|
|
722
|
+
requestArray,
|
|
723
|
+
requests.length
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (!resultPtr) {
|
|
727
|
+
throw new Error("Batch request failed");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Read result struct
|
|
731
|
+
const result = koffi.decode(resultPtr, ZPipelineResult);
|
|
732
|
+
const count = Number(result.count);
|
|
733
|
+
|
|
734
|
+
// Read response array
|
|
735
|
+
const responses: string[] = [];
|
|
736
|
+
const responseArray = koffi.decode(
|
|
737
|
+
result.responses,
|
|
738
|
+
koffi.array(ZPipelineResponse, count)
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
for (let i = 0; i < count; i++) {
|
|
742
|
+
const resp = responseArray[i];
|
|
743
|
+
const bodyLen = Number(resp.body_len);
|
|
744
|
+
if (bodyLen > 0) {
|
|
745
|
+
const bodyBuf = koffi.decode(
|
|
746
|
+
resp.body_ptr,
|
|
747
|
+
koffi.array("uint8_t", bodyLen)
|
|
748
|
+
);
|
|
749
|
+
responses.push(Buffer.from(bodyBuf).toString("utf-8"));
|
|
750
|
+
} else {
|
|
751
|
+
responses.push("");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Free native memory
|
|
756
|
+
lib.symbols.zclient_pipeline_result_free(resultPtr);
|
|
757
|
+
|
|
758
|
+
return responses;
|
|
381
759
|
}
|
|
382
760
|
|
|
383
761
|
public async request<T = any>(
|
|
@@ -443,84 +821,133 @@ export class JirenClient<
|
|
|
443
821
|
}
|
|
444
822
|
}
|
|
445
823
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
"accept-encoding": "gzip",
|
|
452
|
-
"accept-language": "en-US,en;q=0.9",
|
|
453
|
-
"sec-ch-ua":
|
|
454
|
-
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
455
|
-
"sec-ch-ua-mobile": "?0",
|
|
456
|
-
"sec-ch-ua-platform": '"macOS"',
|
|
457
|
-
"sec-fetch-dest": "document",
|
|
458
|
-
"sec-fetch-mode": "navigate",
|
|
459
|
-
"sec-fetch-site": "none",
|
|
460
|
-
"sec-fetch-user": "?1",
|
|
461
|
-
"upgrade-insecure-requests": "1",
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
const finalHeaders = { ...defaultHeaders, ...headers };
|
|
465
|
-
|
|
466
|
-
// Enforce Chrome header order
|
|
467
|
-
const orderedHeaders: Record<string, string> = {};
|
|
468
|
-
const keys = [
|
|
469
|
-
"sec-ch-ua",
|
|
470
|
-
"sec-ch-ua-mobile",
|
|
471
|
-
"sec-ch-ua-platform",
|
|
472
|
-
"upgrade-insecure-requests",
|
|
473
|
-
"user-agent",
|
|
474
|
-
"accept",
|
|
475
|
-
"sec-fetch-site",
|
|
476
|
-
"sec-fetch-mode",
|
|
477
|
-
"sec-fetch-user",
|
|
478
|
-
"sec-fetch-dest",
|
|
479
|
-
"accept-encoding",
|
|
480
|
-
"accept-language",
|
|
481
|
-
];
|
|
482
|
-
|
|
483
|
-
for (const key of keys) {
|
|
484
|
-
if (finalHeaders[key]) {
|
|
485
|
-
orderedHeaders[key] = finalHeaders[key];
|
|
486
|
-
delete finalHeaders[key];
|
|
824
|
+
// Run interceptors
|
|
825
|
+
let ctx: InterceptorRequestContext = { method, url, headers, body };
|
|
826
|
+
if (this.requestInterceptors.length > 0) {
|
|
827
|
+
for (const interceptor of this.requestInterceptors) {
|
|
828
|
+
ctx = await interceptor(ctx);
|
|
487
829
|
}
|
|
830
|
+
method = ctx.method;
|
|
831
|
+
url = ctx.url;
|
|
832
|
+
headers = ctx.headers;
|
|
833
|
+
body = ctx.body ?? null;
|
|
488
834
|
}
|
|
489
|
-
|
|
490
|
-
|
|
835
|
+
|
|
836
|
+
// Prepare headers
|
|
837
|
+
let headerStr: string;
|
|
838
|
+
const hasCustomHeaders = Object.keys(headers).length > 0;
|
|
839
|
+
|
|
840
|
+
if (hasCustomHeaders) {
|
|
841
|
+
const finalHeaders = this.useDefaultHeaders
|
|
842
|
+
? { ...this.defaultHeaders, ...headers }
|
|
843
|
+
: headers;
|
|
844
|
+
const orderedHeaders: Record<string, string> = {};
|
|
845
|
+
const keys = [
|
|
846
|
+
"sec-ch-ua",
|
|
847
|
+
"sec-ch-ua-mobile",
|
|
848
|
+
"sec-ch-ua-platform",
|
|
849
|
+
"upgrade-insecure-requests",
|
|
850
|
+
"user-agent",
|
|
851
|
+
"accept",
|
|
852
|
+
"sec-fetch-site",
|
|
853
|
+
"sec-fetch-mode",
|
|
854
|
+
"sec-fetch-user",
|
|
855
|
+
"sec-fetch-dest",
|
|
856
|
+
"accept-encoding",
|
|
857
|
+
"accept-language",
|
|
858
|
+
];
|
|
859
|
+
|
|
860
|
+
for (const key of keys) {
|
|
861
|
+
if (finalHeaders[key]) {
|
|
862
|
+
orderedHeaders[key] = finalHeaders[key];
|
|
863
|
+
delete finalHeaders[key];
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
868
|
+
orderedHeaders[key] = value;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
headerStr = Object.entries(orderedHeaders)
|
|
872
|
+
.map(([k, v]) => `${k.toLowerCase()}: ${v}`)
|
|
873
|
+
.join("\r\n");
|
|
874
|
+
} else {
|
|
875
|
+
headerStr = this.useDefaultHeaders ? this.defaultHeadersStr : "";
|
|
491
876
|
}
|
|
492
877
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
878
|
+
// Retry logic
|
|
879
|
+
let retryConfig = this.globalRetry;
|
|
880
|
+
if (options && typeof options === "object" && "retry" in options) {
|
|
881
|
+
const userRetry = (options as any).retry;
|
|
882
|
+
if (typeof userRetry === "number") {
|
|
883
|
+
retryConfig = { count: userRetry, delay: 100, backoff: 2 };
|
|
884
|
+
} else if (typeof userRetry === "object") {
|
|
885
|
+
retryConfig = userRetry;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
496
888
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
889
|
+
let attempts = 0;
|
|
890
|
+
const maxAttempts = (retryConfig?.count || 0) + 1;
|
|
891
|
+
let currentDelay = retryConfig?.delay || 100;
|
|
892
|
+
const backoff = retryConfig?.backoff || 2;
|
|
893
|
+
let lastError: any;
|
|
894
|
+
|
|
895
|
+
while (attempts < maxAttempts) {
|
|
896
|
+
attempts++;
|
|
897
|
+
try {
|
|
898
|
+
const respPtr = lib.symbols.zclient_request(
|
|
899
|
+
this.ptr,
|
|
900
|
+
method,
|
|
901
|
+
url,
|
|
902
|
+
headerStr.length > 0 ? headerStr : null,
|
|
903
|
+
body || null,
|
|
904
|
+
maxRedirects,
|
|
905
|
+
antibot
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
if (!respPtr) {
|
|
909
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
910
|
+
}
|
|
503
911
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
912
|
+
const response = this.parseResponse<T>(respPtr, url);
|
|
913
|
+
|
|
914
|
+
// Run response interceptors
|
|
915
|
+
let finalResponse = response;
|
|
916
|
+
if (this.responseInterceptors.length > 0) {
|
|
917
|
+
let responseCtx: InterceptorResponseContext<T> = {
|
|
918
|
+
request: ctx,
|
|
919
|
+
response,
|
|
920
|
+
};
|
|
921
|
+
for (const interceptor of this.responseInterceptors) {
|
|
922
|
+
responseCtx = await interceptor(responseCtx);
|
|
923
|
+
}
|
|
924
|
+
finalResponse = responseCtx.response;
|
|
925
|
+
}
|
|
513
926
|
|
|
514
|
-
|
|
927
|
+
if (responseType) {
|
|
928
|
+
if (responseType === "json") return finalResponse.body.json();
|
|
929
|
+
if (responseType === "text") return finalResponse.body.text();
|
|
930
|
+
if (responseType === "arraybuffer")
|
|
931
|
+
return finalResponse.body.arrayBuffer();
|
|
932
|
+
if (responseType === "blob") return finalResponse.body.blob();
|
|
933
|
+
}
|
|
515
934
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
935
|
+
return finalResponse;
|
|
936
|
+
} catch (err) {
|
|
937
|
+
if (this.errorInterceptors.length > 0) {
|
|
938
|
+
for (const interceptor of this.errorInterceptors) {
|
|
939
|
+
await interceptor(err as Error, ctx);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
lastError = err;
|
|
943
|
+
if (attempts < maxAttempts) {
|
|
944
|
+
await this.waitFor(currentDelay);
|
|
945
|
+
currentDelay *= backoff;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
521
948
|
}
|
|
522
949
|
|
|
523
|
-
|
|
950
|
+
throw lastError || new Error("Request failed after retries");
|
|
524
951
|
}
|
|
525
952
|
|
|
526
953
|
private parseResponse<T = any>(respPtr: any, url: string): JirenResponse<T> {
|
|
@@ -540,8 +967,6 @@ export class JirenClient<
|
|
|
540
967
|
if (headersLen > 0) {
|
|
541
968
|
const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
|
|
542
969
|
if (rawHeadersPtr) {
|
|
543
|
-
// Copy headers to JS memory
|
|
544
|
-
// Koffi decode to buffer: koffi.decode(ptr, "uint8_t", len)
|
|
545
970
|
const raw = Buffer.from(
|
|
546
971
|
koffi.decode(rawHeadersPtr, "uint8_t", headersLen)
|
|
547
972
|
);
|
|
@@ -565,15 +990,12 @@ export class JirenClient<
|
|
|
565
990
|
|
|
566
991
|
let buffer: Buffer = Buffer.alloc(0);
|
|
567
992
|
if (len > 0 && bodyPtr) {
|
|
568
|
-
// Copy body content
|
|
569
993
|
buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
|
|
570
994
|
|
|
571
|
-
// Handle GZIP
|
|
572
|
-
// (handles chunked encoding or other framing that may add prefix bytes)
|
|
995
|
+
// Handle GZIP
|
|
573
996
|
const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
|
|
574
997
|
let gzipOffset = -1;
|
|
575
998
|
|
|
576
|
-
// Search for gzip magic bytes (0x1f 0x8b) in first 16 bytes
|
|
577
999
|
for (let i = 0; i < Math.min(16, buffer.length - 1); i++) {
|
|
578
1000
|
if (buffer[i] === 0x1f && buffer[i + 1] === 0x8b) {
|
|
579
1001
|
gzipOffset = i;
|
|
@@ -583,16 +1005,10 @@ export class JirenClient<
|
|
|
583
1005
|
|
|
584
1006
|
if (contentEncoding === "gzip" || gzipOffset >= 0) {
|
|
585
1007
|
try {
|
|
586
|
-
// If we found gzip at an offset, slice from there
|
|
587
1008
|
const gzipData = gzipOffset > 0 ? buffer.slice(gzipOffset) : buffer;
|
|
588
|
-
console.log(
|
|
589
|
-
`[Jiren] Decompressing gzip (offset: ${
|
|
590
|
-
gzipOffset >= 0 ? gzipOffset : 0
|
|
591
|
-
}, size: ${gzipData.length})`
|
|
592
|
-
);
|
|
593
1009
|
buffer = zlib.gunzipSync(gzipData);
|
|
594
1010
|
} catch (e) {
|
|
595
|
-
|
|
1011
|
+
// Keep original buffer
|
|
596
1012
|
}
|
|
597
1013
|
}
|
|
598
1014
|
}
|
|
@@ -643,6 +1059,33 @@ export class JirenClient<
|
|
|
643
1059
|
lib.symbols.zclient_response_free(respPtr);
|
|
644
1060
|
}
|
|
645
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Helper to prepare body and headers for requests.
|
|
1065
|
+
*/
|
|
1066
|
+
private prepareBody(
|
|
1067
|
+
body: string | object | null | undefined,
|
|
1068
|
+
userHeaders?: Record<string, string>
|
|
1069
|
+
): { headers: Record<string, string>; serializedBody: string | null } {
|
|
1070
|
+
let serializedBody: string | null = null;
|
|
1071
|
+
const headers = { ...userHeaders };
|
|
1072
|
+
|
|
1073
|
+
if (body !== null && body !== undefined) {
|
|
1074
|
+
if (typeof body === "object") {
|
|
1075
|
+
serializedBody = JSON.stringify(body);
|
|
1076
|
+
const hasContentType = Object.keys(headers).some(
|
|
1077
|
+
(k) => k.toLowerCase() === "content-type"
|
|
1078
|
+
);
|
|
1079
|
+
if (!hasContentType) {
|
|
1080
|
+
headers["Content-Type"] = "application/json";
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
serializedBody = String(body);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return { headers, serializedBody };
|
|
1088
|
+
}
|
|
646
1089
|
}
|
|
647
1090
|
|
|
648
1091
|
class NativeHeaders {
|