jiren 1.2.5 → 1.2.7
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/components/client-node-native.ts +655 -0
- package/components/client.ts +185 -55
- package/components/index.ts +2 -0
- package/components/native-node.ts +93 -0
- package/components/types.ts +18 -20
- package/index-node.ts +1 -1
- package/index.ts +5 -1
- package/package.json +4 -1
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { nativeLib as lib } from "./native-node";
|
|
2
|
+
import { ResponseCache } from "./cache";
|
|
3
|
+
import koffi from "koffi";
|
|
4
|
+
import zlib from "zlib";
|
|
5
|
+
import type {
|
|
6
|
+
RequestOptions,
|
|
7
|
+
JirenResponse,
|
|
8
|
+
JirenResponseBody,
|
|
9
|
+
WarmupUrlConfig,
|
|
10
|
+
UrlRequestOptions,
|
|
11
|
+
UrlEndpoint,
|
|
12
|
+
CacheConfig,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
const STATUS_TEXT: Record<number, string> = {
|
|
16
|
+
200: "OK",
|
|
17
|
+
201: "Created",
|
|
18
|
+
204: "No Content",
|
|
19
|
+
301: "Moved Permanently",
|
|
20
|
+
302: "Found",
|
|
21
|
+
400: "Bad Request",
|
|
22
|
+
401: "Unauthorized",
|
|
23
|
+
403: "Forbidden",
|
|
24
|
+
404: "Not Found",
|
|
25
|
+
500: "Internal Server Error",
|
|
26
|
+
502: "Bad Gateway",
|
|
27
|
+
503: "Service Unavailable",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** URL configuration with optional cache */
|
|
31
|
+
export type UrlConfig = string | { url: string; cache?: boolean | CacheConfig };
|
|
32
|
+
|
|
33
|
+
/** Options for JirenClient constructor */
|
|
34
|
+
export interface JirenClientOptions<
|
|
35
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
36
|
+
| readonly WarmupUrlConfig[]
|
|
37
|
+
| Record<string, UrlConfig>
|
|
38
|
+
> {
|
|
39
|
+
/** URLs to warmup on client creation (pre-connect + handshake) */
|
|
40
|
+
warmup?: string[] | T;
|
|
41
|
+
|
|
42
|
+
/** Enable benchmark mode (Force HTTP/2, disable probing) */
|
|
43
|
+
benchmark?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Helper to extract keys from Warmup Config */
|
|
47
|
+
export type ExtractWarmupKeys<
|
|
48
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
49
|
+
> = T extends readonly WarmupUrlConfig[]
|
|
50
|
+
? T[number]["key"]
|
|
51
|
+
: T extends Record<string, UrlConfig>
|
|
52
|
+
? keyof T
|
|
53
|
+
: never;
|
|
54
|
+
|
|
55
|
+
/** Type-safe URL accessor - maps keys to UrlEndpoint objects */
|
|
56
|
+
export type UrlAccessor<
|
|
57
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
58
|
+
> = {
|
|
59
|
+
[K in ExtractWarmupKeys<T>]: UrlEndpoint;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Helper function to define warmup URLs with type inference.
|
|
64
|
+
*/
|
|
65
|
+
export function defineUrls<const T extends readonly WarmupUrlConfig[]>(
|
|
66
|
+
urls: T
|
|
67
|
+
): T {
|
|
68
|
+
return urls;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class JirenClient<
|
|
72
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
73
|
+
| readonly WarmupUrlConfig[]
|
|
74
|
+
| Record<string, UrlConfig>
|
|
75
|
+
> {
|
|
76
|
+
private ptr: any = null; // Koffi pointer (Buffer or External)
|
|
77
|
+
private urlMap: Map<string, string> = new Map();
|
|
78
|
+
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
79
|
+
new Map();
|
|
80
|
+
private cache: ResponseCache;
|
|
81
|
+
|
|
82
|
+
/** Type-safe URL accessor for warmed-up URLs */
|
|
83
|
+
public readonly url: UrlAccessor<T>;
|
|
84
|
+
|
|
85
|
+
constructor(options?: JirenClientOptions<T>) {
|
|
86
|
+
this.ptr = lib.symbols.zclient_new();
|
|
87
|
+
if (!this.ptr) throw new Error("Failed to create native client instance");
|
|
88
|
+
|
|
89
|
+
// Initialize cache
|
|
90
|
+
this.cache = new ResponseCache(100);
|
|
91
|
+
|
|
92
|
+
// Enable benchmark mode if requested
|
|
93
|
+
if (options?.benchmark) {
|
|
94
|
+
lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Process warmup URLs
|
|
98
|
+
if (options?.warmup) {
|
|
99
|
+
const urls: string[] = [];
|
|
100
|
+
const warmup = options.warmup;
|
|
101
|
+
|
|
102
|
+
if (Array.isArray(warmup)) {
|
|
103
|
+
for (const item of warmup) {
|
|
104
|
+
if (typeof item === "string") {
|
|
105
|
+
urls.push(item);
|
|
106
|
+
} else {
|
|
107
|
+
// WarmupUrlConfig with key and optional cache
|
|
108
|
+
const config = item as WarmupUrlConfig;
|
|
109
|
+
urls.push(config.url);
|
|
110
|
+
this.urlMap.set(config.key, config.url);
|
|
111
|
+
|
|
112
|
+
// Store cache config
|
|
113
|
+
if (config.cache) {
|
|
114
|
+
const cacheConfig =
|
|
115
|
+
typeof config.cache === "boolean"
|
|
116
|
+
? { enabled: true, ttl: 60000 }
|
|
117
|
+
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
118
|
+
this.cacheConfig.set(config.key, cacheConfig);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// Record<string, UrlConfig>
|
|
124
|
+
for (const [key, urlConfig] of Object.entries(warmup)) {
|
|
125
|
+
if (typeof urlConfig === "string") {
|
|
126
|
+
// Simple string URL
|
|
127
|
+
urls.push(urlConfig);
|
|
128
|
+
this.urlMap.set(key, urlConfig);
|
|
129
|
+
} else {
|
|
130
|
+
// URL config object with cache
|
|
131
|
+
urls.push(urlConfig.url);
|
|
132
|
+
this.urlMap.set(key, urlConfig.url);
|
|
133
|
+
|
|
134
|
+
// Store cache config
|
|
135
|
+
if (urlConfig.cache) {
|
|
136
|
+
const cacheConfig =
|
|
137
|
+
typeof urlConfig.cache === "boolean"
|
|
138
|
+
? { enabled: true, ttl: 60000 }
|
|
139
|
+
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
140
|
+
this.cacheConfig.set(key, cacheConfig);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (urls.length > 0) {
|
|
147
|
+
this.warmup(urls);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create proxy for type-safe URL access
|
|
152
|
+
this.url = this.createUrlAccessor();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates a proxy-based URL accessor for type-safe access.
|
|
157
|
+
*/
|
|
158
|
+
private createUrlAccessor(): UrlAccessor<T> {
|
|
159
|
+
const self = this;
|
|
160
|
+
|
|
161
|
+
return new Proxy({} as UrlAccessor<T>, {
|
|
162
|
+
get(_target, prop: string) {
|
|
163
|
+
const baseUrl = self.urlMap.get(prop);
|
|
164
|
+
if (!baseUrl) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`URL key "${prop}" not found. Available keys: ${Array.from(
|
|
167
|
+
self.urlMap.keys()
|
|
168
|
+
).join(", ")}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const buildUrl = (path?: string) =>
|
|
173
|
+
path
|
|
174
|
+
? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
|
|
175
|
+
: baseUrl;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
get: async <R = any>(
|
|
179
|
+
options?: UrlRequestOptions
|
|
180
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
181
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
182
|
+
|
|
183
|
+
if (cacheConfig?.enabled) {
|
|
184
|
+
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
185
|
+
if (cached) {
|
|
186
|
+
return cached as any;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const response = await self.request<R>(
|
|
191
|
+
"GET",
|
|
192
|
+
buildUrl(options?.path),
|
|
193
|
+
null,
|
|
194
|
+
{
|
|
195
|
+
headers: options?.headers,
|
|
196
|
+
maxRedirects: options?.maxRedirects,
|
|
197
|
+
responseType: options?.responseType,
|
|
198
|
+
antibot: options?.antibot,
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
cacheConfig?.enabled &&
|
|
204
|
+
typeof response === "object" &&
|
|
205
|
+
"status" in response
|
|
206
|
+
) {
|
|
207
|
+
self.cache.set(
|
|
208
|
+
baseUrl,
|
|
209
|
+
response as JirenResponse,
|
|
210
|
+
cacheConfig.ttl,
|
|
211
|
+
options?.path,
|
|
212
|
+
options
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return response;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
post: async <R = any>(
|
|
220
|
+
body?: string | null,
|
|
221
|
+
options?: UrlRequestOptions
|
|
222
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
223
|
+
return self.request<R>(
|
|
224
|
+
"POST",
|
|
225
|
+
buildUrl(options?.path),
|
|
226
|
+
body || null,
|
|
227
|
+
{
|
|
228
|
+
headers: options?.headers,
|
|
229
|
+
maxRedirects: options?.maxRedirects,
|
|
230
|
+
responseType: options?.responseType,
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
put: async <R = any>(
|
|
236
|
+
body?: string | null,
|
|
237
|
+
options?: UrlRequestOptions
|
|
238
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
239
|
+
return self.request<R>(
|
|
240
|
+
"PUT",
|
|
241
|
+
buildUrl(options?.path),
|
|
242
|
+
body || null,
|
|
243
|
+
{
|
|
244
|
+
headers: options?.headers,
|
|
245
|
+
maxRedirects: options?.maxRedirects,
|
|
246
|
+
responseType: options?.responseType,
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
patch: async <R = any>(
|
|
252
|
+
body?: string | null,
|
|
253
|
+
options?: UrlRequestOptions
|
|
254
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
255
|
+
return self.request<R>(
|
|
256
|
+
"PATCH",
|
|
257
|
+
buildUrl(options?.path),
|
|
258
|
+
body || null,
|
|
259
|
+
{
|
|
260
|
+
headers: options?.headers,
|
|
261
|
+
maxRedirects: options?.maxRedirects,
|
|
262
|
+
responseType: options?.responseType,
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
delete: async <R = any>(
|
|
268
|
+
body?: string | null,
|
|
269
|
+
options?: UrlRequestOptions
|
|
270
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
271
|
+
return self.request<R>(
|
|
272
|
+
"DELETE",
|
|
273
|
+
buildUrl(options?.path),
|
|
274
|
+
body || null,
|
|
275
|
+
{
|
|
276
|
+
headers: options?.headers,
|
|
277
|
+
maxRedirects: options?.maxRedirects,
|
|
278
|
+
responseType: options?.responseType,
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
head: async (
|
|
284
|
+
options?: UrlRequestOptions
|
|
285
|
+
): Promise<JirenResponse<any>> => {
|
|
286
|
+
return self.request("HEAD", buildUrl(options?.path), null, {
|
|
287
|
+
headers: options?.headers,
|
|
288
|
+
maxRedirects: options?.maxRedirects,
|
|
289
|
+
antibot: options?.antibot,
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
options: async (
|
|
294
|
+
options?: UrlRequestOptions
|
|
295
|
+
): Promise<JirenResponse<any>> => {
|
|
296
|
+
return self.request("OPTIONS", buildUrl(options?.path), null, {
|
|
297
|
+
headers: options?.headers,
|
|
298
|
+
maxRedirects: options?.maxRedirects,
|
|
299
|
+
antibot: options?.antibot,
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
prefetch: async (options?: UrlRequestOptions): Promise<void> => {
|
|
304
|
+
self.cache.clear(baseUrl);
|
|
305
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
306
|
+
if (cacheConfig?.enabled) {
|
|
307
|
+
await self.request("GET", buildUrl(options?.path), null, {
|
|
308
|
+
headers: options?.headers,
|
|
309
|
+
maxRedirects: options?.maxRedirects,
|
|
310
|
+
antibot: options?.antibot,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
} as UrlEndpoint;
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public close(): void {
|
|
320
|
+
if (this.ptr) {
|
|
321
|
+
lib.symbols.zclient_free(this.ptr);
|
|
322
|
+
this.ptr = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public async warmup(urls: string[]): Promise<void> {
|
|
327
|
+
if (!this.ptr) throw new Error("Client is closed");
|
|
328
|
+
|
|
329
|
+
await Promise.all(
|
|
330
|
+
urls.map(
|
|
331
|
+
(url) =>
|
|
332
|
+
new Promise<void>((resolve) => {
|
|
333
|
+
const urlBuffer = Buffer.from(url + "\0");
|
|
334
|
+
lib.symbols.zclient_prefetch(this.ptr, url); // Koffi handles string auto-conversion if type is 'const char*' but Buffer is safer for null termination?
|
|
335
|
+
// Koffi: "const char *" expects a string or Buffer. String is null-terminated by Koffi.
|
|
336
|
+
// Using generic string.
|
|
337
|
+
resolve();
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @deprecated Use warmup() instead
|
|
345
|
+
*/
|
|
346
|
+
public prefetch(urls: string[]): void {
|
|
347
|
+
this.warmup(urls);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public async request<T = any>(
|
|
351
|
+
method: string,
|
|
352
|
+
url: string,
|
|
353
|
+
body?: string | null,
|
|
354
|
+
options?: RequestOptions & { responseType: "json" }
|
|
355
|
+
): Promise<T>;
|
|
356
|
+
public async request<T = any>(
|
|
357
|
+
method: string,
|
|
358
|
+
url: string,
|
|
359
|
+
body?: string | null,
|
|
360
|
+
options?: RequestOptions & { responseType: "text" }
|
|
361
|
+
): Promise<string>;
|
|
362
|
+
public async request<T = any>(
|
|
363
|
+
method: string,
|
|
364
|
+
url: string,
|
|
365
|
+
body?: string | null,
|
|
366
|
+
options?: RequestOptions & { responseType: "arraybuffer" }
|
|
367
|
+
): Promise<ArrayBuffer>;
|
|
368
|
+
public async request<T = any>(
|
|
369
|
+
method: string,
|
|
370
|
+
url: string,
|
|
371
|
+
body?: string | null,
|
|
372
|
+
options?: RequestOptions & { responseType: "blob" }
|
|
373
|
+
): Promise<Blob>;
|
|
374
|
+
public async request<T = any>(
|
|
375
|
+
method: string,
|
|
376
|
+
url: string,
|
|
377
|
+
body?: string | null,
|
|
378
|
+
options?: RequestOptions
|
|
379
|
+
): Promise<JirenResponse<T>>;
|
|
380
|
+
public async request<T = any>(
|
|
381
|
+
method: string,
|
|
382
|
+
url: string,
|
|
383
|
+
body?: string | null,
|
|
384
|
+
options?: RequestOptions | Record<string, string> | null
|
|
385
|
+
): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
|
|
386
|
+
if (!this.ptr) throw new Error("Client is closed");
|
|
387
|
+
|
|
388
|
+
// Normalize options
|
|
389
|
+
let headers: Record<string, string> = {};
|
|
390
|
+
let maxRedirects = 5;
|
|
391
|
+
let responseType: RequestOptions["responseType"] | undefined;
|
|
392
|
+
let antibot = false;
|
|
393
|
+
|
|
394
|
+
if (options) {
|
|
395
|
+
if (
|
|
396
|
+
"maxRedirects" in options ||
|
|
397
|
+
"headers" in options ||
|
|
398
|
+
"responseType" in options ||
|
|
399
|
+
"method" in options ||
|
|
400
|
+
"timeout" in options ||
|
|
401
|
+
"antibot" in options
|
|
402
|
+
) {
|
|
403
|
+
const opts = options as RequestOptions;
|
|
404
|
+
if (opts.headers) headers = opts.headers;
|
|
405
|
+
if (opts.maxRedirects !== undefined) maxRedirects = opts.maxRedirects;
|
|
406
|
+
if (opts.responseType) responseType = opts.responseType;
|
|
407
|
+
if (opts.antibot !== undefined) antibot = opts.antibot;
|
|
408
|
+
} else {
|
|
409
|
+
headers = options as Record<string, string>;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const defaultHeaders: Record<string, string> = {
|
|
414
|
+
"user-agent":
|
|
415
|
+
"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",
|
|
416
|
+
accept:
|
|
417
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
418
|
+
"accept-encoding": "gzip",
|
|
419
|
+
"accept-language": "en-US,en;q=0.9",
|
|
420
|
+
"sec-ch-ua":
|
|
421
|
+
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
422
|
+
"sec-ch-ua-mobile": "?0",
|
|
423
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
424
|
+
"sec-fetch-dest": "document",
|
|
425
|
+
"sec-fetch-mode": "navigate",
|
|
426
|
+
"sec-fetch-site": "none",
|
|
427
|
+
"sec-fetch-user": "?1",
|
|
428
|
+
"upgrade-insecure-requests": "1",
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const finalHeaders = { ...defaultHeaders, ...headers };
|
|
432
|
+
|
|
433
|
+
// Enforce Chrome header order
|
|
434
|
+
const orderedHeaders: Record<string, string> = {};
|
|
435
|
+
const keys = [
|
|
436
|
+
"sec-ch-ua",
|
|
437
|
+
"sec-ch-ua-mobile",
|
|
438
|
+
"sec-ch-ua-platform",
|
|
439
|
+
"upgrade-insecure-requests",
|
|
440
|
+
"user-agent",
|
|
441
|
+
"accept",
|
|
442
|
+
"sec-fetch-site",
|
|
443
|
+
"sec-fetch-mode",
|
|
444
|
+
"sec-fetch-user",
|
|
445
|
+
"sec-fetch-dest",
|
|
446
|
+
"accept-encoding",
|
|
447
|
+
"accept-language",
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
for (const key of keys) {
|
|
451
|
+
if (finalHeaders[key]) {
|
|
452
|
+
orderedHeaders[key] = finalHeaders[key];
|
|
453
|
+
delete finalHeaders[key];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
457
|
+
orderedHeaders[key] = value;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const headerStr = Object.entries(orderedHeaders)
|
|
461
|
+
.map(([k, v]) => `${k.toLowerCase()}: ${v}`)
|
|
462
|
+
.join("\r\n");
|
|
463
|
+
|
|
464
|
+
// Koffi auto-converts strings to const char *.
|
|
465
|
+
// Pass strings directly.
|
|
466
|
+
// For body and headers, empty string is fine if null.
|
|
467
|
+
// But native check might expect null pointer?
|
|
468
|
+
// zclient_request expects const char*. If passed null/undefined, Koffi might pass NULL?
|
|
469
|
+
// Let's pass null for explicit nulls.
|
|
470
|
+
|
|
471
|
+
const respPtr = lib.symbols.zclient_request(
|
|
472
|
+
this.ptr,
|
|
473
|
+
method,
|
|
474
|
+
url,
|
|
475
|
+
headerStr.length > 0 ? headerStr : null,
|
|
476
|
+
body || null,
|
|
477
|
+
maxRedirects,
|
|
478
|
+
antibot
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const response = this.parseResponse<T>(respPtr, url);
|
|
482
|
+
|
|
483
|
+
if (responseType) {
|
|
484
|
+
if (responseType === "json") return response.body.json();
|
|
485
|
+
if (responseType === "text") return response.body.text();
|
|
486
|
+
if (responseType === "arraybuffer") return response.body.arrayBuffer();
|
|
487
|
+
if (responseType === "blob") return response.body.blob();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return response;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private parseResponse<T = any>(respPtr: any, url: string): JirenResponse<T> {
|
|
494
|
+
if (!respPtr)
|
|
495
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const status = lib.symbols.zclient_response_status(respPtr);
|
|
499
|
+
const len = Number(lib.symbols.zclient_response_body_len(respPtr));
|
|
500
|
+
const bodyPtr = lib.symbols.zclient_response_body(respPtr);
|
|
501
|
+
|
|
502
|
+
const headersLen = Number(
|
|
503
|
+
lib.symbols.zclient_response_headers_len(respPtr)
|
|
504
|
+
);
|
|
505
|
+
let headersObj: Record<string, string> | NativeHeaders = {};
|
|
506
|
+
|
|
507
|
+
if (headersLen > 0) {
|
|
508
|
+
const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
|
|
509
|
+
if (rawHeadersPtr) {
|
|
510
|
+
// Copy headers to JS memory
|
|
511
|
+
// Koffi decode to buffer: koffi.decode(ptr, "uint8_t", len)
|
|
512
|
+
const raw = Buffer.from(
|
|
513
|
+
koffi.decode(rawHeadersPtr, "uint8_t", headersLen)
|
|
514
|
+
);
|
|
515
|
+
headersObj = new NativeHeaders(raw);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const headersProxy = new Proxy(
|
|
520
|
+
headersObj instanceof NativeHeaders ? headersObj : {},
|
|
521
|
+
{
|
|
522
|
+
get(target, prop) {
|
|
523
|
+
if (target instanceof NativeHeaders && typeof prop === "string") {
|
|
524
|
+
if (prop === "toJSON") return () => target.toJSON();
|
|
525
|
+
const val = target.get(prop);
|
|
526
|
+
if (val !== null) return val;
|
|
527
|
+
}
|
|
528
|
+
return Reflect.get(target, prop);
|
|
529
|
+
},
|
|
530
|
+
}
|
|
531
|
+
) as unknown as Record<string, string>;
|
|
532
|
+
|
|
533
|
+
let buffer: Buffer = Buffer.alloc(0);
|
|
534
|
+
if (len > 0 && bodyPtr) {
|
|
535
|
+
// Copy body content
|
|
536
|
+
buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
|
|
537
|
+
|
|
538
|
+
// Handle GZIP compression
|
|
539
|
+
const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
|
|
540
|
+
|
|
541
|
+
// DEBUG LOGS
|
|
542
|
+
if (len > 0) {
|
|
543
|
+
console.log(`[Jiren] Body len: ${len}, Encoding: ${contentEncoding}`);
|
|
544
|
+
if (buffer.length > 2) {
|
|
545
|
+
console.log(
|
|
546
|
+
`[Jiren] Bytes: 0x${buffer[0]?.toString(
|
|
547
|
+
16
|
|
548
|
+
)} 0x${buffer[1]?.toString(16)} 0x${buffer[2]?.toString(16)}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
contentEncoding === "gzip" ||
|
|
555
|
+
(buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b)
|
|
556
|
+
) {
|
|
557
|
+
try {
|
|
558
|
+
console.log("[Jiren] Attempting gunzip...");
|
|
559
|
+
buffer = zlib.gunzipSync(buffer);
|
|
560
|
+
console.log("[Jiren] Gunzip success!");
|
|
561
|
+
} catch (e) {
|
|
562
|
+
console.warn("Failed to gunzip response body:", e);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let bodyUsed = false;
|
|
568
|
+
const consumeBody = () => {
|
|
569
|
+
bodyUsed = true;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
573
|
+
bodyUsed: false,
|
|
574
|
+
arrayBuffer: async () => {
|
|
575
|
+
consumeBody();
|
|
576
|
+
return buffer.buffer.slice(
|
|
577
|
+
buffer.byteOffset,
|
|
578
|
+
buffer.byteOffset + buffer.byteLength
|
|
579
|
+
) as ArrayBuffer;
|
|
580
|
+
},
|
|
581
|
+
blob: async () => {
|
|
582
|
+
consumeBody();
|
|
583
|
+
return new Blob([buffer as any]);
|
|
584
|
+
},
|
|
585
|
+
text: async () => {
|
|
586
|
+
consumeBody();
|
|
587
|
+
return buffer.toString("utf-8");
|
|
588
|
+
},
|
|
589
|
+
json: async <R = T>(): Promise<R> => {
|
|
590
|
+
consumeBody();
|
|
591
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
596
|
+
get: () => bodyUsed,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
url,
|
|
601
|
+
status,
|
|
602
|
+
statusText: STATUS_TEXT[status] || "",
|
|
603
|
+
headers: headersProxy,
|
|
604
|
+
ok: status >= 200 && status < 300,
|
|
605
|
+
redirected: false,
|
|
606
|
+
type: "basic",
|
|
607
|
+
body: bodyObj,
|
|
608
|
+
} as JirenResponse<T>;
|
|
609
|
+
} finally {
|
|
610
|
+
lib.symbols.zclient_response_free(respPtr);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
class NativeHeaders {
|
|
616
|
+
private raw: Buffer;
|
|
617
|
+
private len: number;
|
|
618
|
+
private cache = new Map<string, string>();
|
|
619
|
+
private parsed = false;
|
|
620
|
+
|
|
621
|
+
constructor(raw: Buffer) {
|
|
622
|
+
this.raw = raw;
|
|
623
|
+
this.len = raw.length;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private ensureParsed() {
|
|
627
|
+
if (this.parsed) return;
|
|
628
|
+
try {
|
|
629
|
+
const text = this.raw.toString("utf-8");
|
|
630
|
+
const lines = text.split("\r\n");
|
|
631
|
+
for (const line of lines) {
|
|
632
|
+
if (!line) continue;
|
|
633
|
+
const colon = line.indexOf(":");
|
|
634
|
+
if (colon === -1) continue;
|
|
635
|
+
const key = line.substring(0, colon).trim().toLowerCase();
|
|
636
|
+
const val = line.substring(colon + 1).trim();
|
|
637
|
+
this.cache.set(key, val);
|
|
638
|
+
}
|
|
639
|
+
} catch (e) {
|
|
640
|
+
// Ignore parsing errors
|
|
641
|
+
}
|
|
642
|
+
this.parsed = true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
get(name: string): string | null {
|
|
646
|
+
const target = name.toLowerCase();
|
|
647
|
+
this.ensureParsed();
|
|
648
|
+
return this.cache.get(target) || null;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
toJSON(): Record<string, string> {
|
|
652
|
+
this.ensureParsed();
|
|
653
|
+
return Object.fromEntries(this.cache);
|
|
654
|
+
}
|
|
655
|
+
}
|
package/components/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
UrlRequestOptions,
|
|
10
10
|
UrlEndpoint,
|
|
11
11
|
CacheConfig,
|
|
12
|
+
RetryConfig,
|
|
12
13
|
} from "./types";
|
|
13
14
|
|
|
14
15
|
const STATUS_TEXT: Record<number, string> = {
|
|
@@ -40,6 +41,9 @@ export interface JirenClientOptions<
|
|
|
40
41
|
|
|
41
42
|
/** Enable benchmark mode (Force HTTP/2, disable probing) */
|
|
42
43
|
benchmark?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Global retry configuration */
|
|
46
|
+
retry?: number | RetryConfig;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/** Helper to extract keys from Warmup Config */
|
|
@@ -93,6 +97,8 @@ export class JirenClient<
|
|
|
93
97
|
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
94
98
|
new Map();
|
|
95
99
|
private cache: ResponseCache;
|
|
100
|
+
private inflightRequests: Map<string, Promise<any>> = new Map();
|
|
101
|
+
private globalRetry?: RetryConfig;
|
|
96
102
|
|
|
97
103
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
98
104
|
public readonly url: UrlAccessor<T>;
|
|
@@ -165,6 +171,18 @@ export class JirenClient<
|
|
|
165
171
|
|
|
166
172
|
// Create proxy for type-safe URL access
|
|
167
173
|
this.url = this.createUrlAccessor();
|
|
174
|
+
|
|
175
|
+
// Store global retry config
|
|
176
|
+
if (options?.retry) {
|
|
177
|
+
this.globalRetry =
|
|
178
|
+
typeof options.retry === "number"
|
|
179
|
+
? { count: options.retry, delay: 100, backoff: 2 }
|
|
180
|
+
: options.retry;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async waitFor(ms: number) {
|
|
185
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
/**
|
|
@@ -206,47 +224,74 @@ export class JirenClient<
|
|
|
206
224
|
}
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
headers: options?.headers,
|
|
216
|
-
maxRedirects: options?.maxRedirects,
|
|
217
|
-
responseType: options?.responseType,
|
|
218
|
-
antibot: options?.antibot,
|
|
219
|
-
}
|
|
220
|
-
);
|
|
227
|
+
// ** Deduplication Logic **
|
|
228
|
+
// Create a unique key for this request based on URL and critical options
|
|
229
|
+
const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(
|
|
230
|
+
options?.headers || {}
|
|
231
|
+
)}`;
|
|
221
232
|
|
|
222
|
-
//
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
typeof response === "object" &&
|
|
226
|
-
"status" in response
|
|
227
|
-
) {
|
|
228
|
-
self.cache.set(
|
|
229
|
-
baseUrl,
|
|
230
|
-
response as JirenResponse,
|
|
231
|
-
cacheConfig.ttl,
|
|
232
|
-
options?.path,
|
|
233
|
-
options
|
|
234
|
-
);
|
|
233
|
+
// Check if there is already an identical request in flight
|
|
234
|
+
if (self.inflightRequests.has(dedupKey)) {
|
|
235
|
+
return self.inflightRequests.get(dedupKey);
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
// Create the request promise
|
|
239
|
+
const requestPromise = (async () => {
|
|
240
|
+
try {
|
|
241
|
+
// Make the request
|
|
242
|
+
const response = await self.request<R>(
|
|
243
|
+
"GET",
|
|
244
|
+
buildUrl(options?.path),
|
|
245
|
+
null,
|
|
246
|
+
{
|
|
247
|
+
headers: options?.headers,
|
|
248
|
+
maxRedirects: options?.maxRedirects,
|
|
249
|
+
responseType: options?.responseType,
|
|
250
|
+
antibot: options?.antibot,
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Store in cache if enabled
|
|
255
|
+
if (
|
|
256
|
+
cacheConfig?.enabled &&
|
|
257
|
+
typeof response === "object" &&
|
|
258
|
+
"status" in response
|
|
259
|
+
) {
|
|
260
|
+
self.cache.set(
|
|
261
|
+
baseUrl,
|
|
262
|
+
response as JirenResponse,
|
|
263
|
+
cacheConfig.ttl,
|
|
264
|
+
options?.path,
|
|
265
|
+
options
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return response;
|
|
270
|
+
} finally {
|
|
271
|
+
// Remove from inflight map when done (success or failure)
|
|
272
|
+
self.inflightRequests.delete(dedupKey);
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
// Store the promise in the map
|
|
277
|
+
self.inflightRequests.set(dedupKey, requestPromise);
|
|
278
|
+
|
|
279
|
+
return requestPromise;
|
|
238
280
|
},
|
|
239
281
|
|
|
240
282
|
post: async <R = any>(
|
|
241
|
-
body?: string | null,
|
|
242
283
|
options?: UrlRequestOptions
|
|
243
284
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
285
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
286
|
+
options?.body,
|
|
287
|
+
options?.headers
|
|
288
|
+
);
|
|
244
289
|
return self.request<R>(
|
|
245
290
|
"POST",
|
|
246
291
|
buildUrl(options?.path),
|
|
247
|
-
|
|
292
|
+
serializedBody,
|
|
248
293
|
{
|
|
249
|
-
headers
|
|
294
|
+
headers,
|
|
250
295
|
maxRedirects: options?.maxRedirects,
|
|
251
296
|
responseType: options?.responseType,
|
|
252
297
|
}
|
|
@@ -254,15 +299,18 @@ export class JirenClient<
|
|
|
254
299
|
},
|
|
255
300
|
|
|
256
301
|
put: async <R = any>(
|
|
257
|
-
body?: string | null,
|
|
258
302
|
options?: UrlRequestOptions
|
|
259
303
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
304
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
305
|
+
options?.body,
|
|
306
|
+
options?.headers
|
|
307
|
+
);
|
|
260
308
|
return self.request<R>(
|
|
261
309
|
"PUT",
|
|
262
310
|
buildUrl(options?.path),
|
|
263
|
-
|
|
311
|
+
serializedBody,
|
|
264
312
|
{
|
|
265
|
-
headers
|
|
313
|
+
headers,
|
|
266
314
|
maxRedirects: options?.maxRedirects,
|
|
267
315
|
responseType: options?.responseType,
|
|
268
316
|
}
|
|
@@ -270,15 +318,18 @@ export class JirenClient<
|
|
|
270
318
|
},
|
|
271
319
|
|
|
272
320
|
patch: async <R = any>(
|
|
273
|
-
body?: string | null,
|
|
274
321
|
options?: UrlRequestOptions
|
|
275
322
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
323
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
324
|
+
options?.body,
|
|
325
|
+
options?.headers
|
|
326
|
+
);
|
|
276
327
|
return self.request<R>(
|
|
277
328
|
"PATCH",
|
|
278
329
|
buildUrl(options?.path),
|
|
279
|
-
|
|
330
|
+
serializedBody,
|
|
280
331
|
{
|
|
281
|
-
headers
|
|
332
|
+
headers,
|
|
282
333
|
maxRedirects: options?.maxRedirects,
|
|
283
334
|
responseType: options?.responseType,
|
|
284
335
|
}
|
|
@@ -286,15 +337,18 @@ export class JirenClient<
|
|
|
286
337
|
},
|
|
287
338
|
|
|
288
339
|
delete: async <R = any>(
|
|
289
|
-
body?: string | null,
|
|
290
340
|
options?: UrlRequestOptions
|
|
291
341
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
342
|
+
const { headers, serializedBody } = self.prepareBody(
|
|
343
|
+
options?.body,
|
|
344
|
+
options?.headers
|
|
345
|
+
);
|
|
292
346
|
return self.request<R>(
|
|
293
347
|
"DELETE",
|
|
294
348
|
buildUrl(options?.path),
|
|
295
|
-
|
|
349
|
+
serializedBody,
|
|
296
350
|
{
|
|
297
|
-
headers
|
|
351
|
+
headers,
|
|
298
352
|
maxRedirects: options?.maxRedirects,
|
|
299
353
|
responseType: options?.responseType,
|
|
300
354
|
}
|
|
@@ -523,27 +577,74 @@ export class JirenClient<
|
|
|
523
577
|
headersBuffer = Buffer.from(headerStr + "\0");
|
|
524
578
|
}
|
|
525
579
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
580
|
+
// Determine retry configuration
|
|
581
|
+
let retryConfig = this.globalRetry;
|
|
582
|
+
// Check if options is RequestOptions (has 'retry' property and is not just a header map)
|
|
583
|
+
// We already normalized this earlier, but let's be safe.
|
|
584
|
+
// If it has 'responseType', 'method', etc., it's RequestOptions.
|
|
585
|
+
// Simpler: Just check if 'retry' is number or object.
|
|
586
|
+
if (options && typeof options === "object" && "retry" in options) {
|
|
587
|
+
const userRetry = (options as any).retry;
|
|
588
|
+
if (typeof userRetry === "number") {
|
|
589
|
+
retryConfig = { count: userRetry, delay: 100, backoff: 2 };
|
|
590
|
+
} else if (typeof userRetry === "object") {
|
|
591
|
+
retryConfig = userRetry;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
535
594
|
|
|
536
|
-
|
|
595
|
+
let attempts = 0;
|
|
596
|
+
// Default to 1 attempt (0 retries) if no config
|
|
597
|
+
const maxAttempts = (retryConfig?.count || 0) + 1;
|
|
598
|
+
let currentDelay = retryConfig?.delay || 100;
|
|
599
|
+
const backoff = retryConfig?.backoff || 2;
|
|
600
|
+
|
|
601
|
+
let lastError: any;
|
|
602
|
+
|
|
603
|
+
while (attempts < maxAttempts) {
|
|
604
|
+
attempts++;
|
|
605
|
+
try {
|
|
606
|
+
const respPtr = lib.symbols.zclient_request(
|
|
607
|
+
this.ptr,
|
|
608
|
+
methodBuffer,
|
|
609
|
+
urlBuffer,
|
|
610
|
+
headersBuffer,
|
|
611
|
+
bodyBuffer,
|
|
612
|
+
maxRedirects,
|
|
613
|
+
antibot
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
if (!respPtr) {
|
|
617
|
+
throw new Error("Native request failed (returned null pointer)");
|
|
618
|
+
}
|
|
537
619
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
620
|
+
const response = this.parseResponse<T>(respPtr, url);
|
|
621
|
+
|
|
622
|
+
// Optional: Retry on specific status codes (e.g., 500, 502, 503, 504)
|
|
623
|
+
// For now, we only retry on actual exceptions/network failures (null ptr)
|
|
624
|
+
// or if we decide to throw on 5xx here.
|
|
625
|
+
// Let's stick to "Network Failure" retries for now as per plan.
|
|
626
|
+
|
|
627
|
+
// Auto-parse if requested
|
|
628
|
+
if (responseType) {
|
|
629
|
+
if (responseType === "json") return response.body.json();
|
|
630
|
+
if (responseType === "text") return response.body.text();
|
|
631
|
+
if (responseType === "arraybuffer")
|
|
632
|
+
return response.body.arrayBuffer();
|
|
633
|
+
if (responseType === "blob") return response.body.blob();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return response;
|
|
637
|
+
} catch (err) {
|
|
638
|
+
lastError = err;
|
|
639
|
+
if (attempts < maxAttempts) {
|
|
640
|
+
// Wait before retrying
|
|
641
|
+
await this.waitFor(currentDelay);
|
|
642
|
+
currentDelay *= backoff;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
544
645
|
}
|
|
545
646
|
|
|
546
|
-
|
|
647
|
+
throw lastError || new Error("Request failed after retries");
|
|
547
648
|
}
|
|
548
649
|
|
|
549
650
|
private parseResponse<T = any>(
|
|
@@ -652,6 +753,35 @@ export class JirenClient<
|
|
|
652
753
|
lib.symbols.zclient_response_free(respPtr);
|
|
653
754
|
}
|
|
654
755
|
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Helper to prepare body and headers for requests.
|
|
759
|
+
* Handles JSON stringification and Content-Type header.
|
|
760
|
+
*/
|
|
761
|
+
private prepareBody(
|
|
762
|
+
body: string | object | null | undefined,
|
|
763
|
+
userHeaders?: Record<string, string>
|
|
764
|
+
): { headers: Record<string, string>; serializedBody: string | null } {
|
|
765
|
+
let serializedBody: string | null = null;
|
|
766
|
+
const headers = { ...userHeaders };
|
|
767
|
+
|
|
768
|
+
if (body !== null && body !== undefined) {
|
|
769
|
+
if (typeof body === "object") {
|
|
770
|
+
serializedBody = JSON.stringify(body);
|
|
771
|
+
// Add Content-Type if not present (case-insensitive check)
|
|
772
|
+
const hasContentType = Object.keys(headers).some(
|
|
773
|
+
(k) => k.toLowerCase() === "content-type"
|
|
774
|
+
);
|
|
775
|
+
if (!hasContentType) {
|
|
776
|
+
headers["Content-Type"] = "application/json";
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
serializedBody = String(body);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return { headers, serializedBody };
|
|
784
|
+
}
|
|
655
785
|
}
|
|
656
786
|
|
|
657
787
|
class NativeHeaders {
|
package/components/index.ts
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import koffi from "koffi";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// Determine library extension based on platform
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
let suffix = "so";
|
|
11
|
+
if (platform === "darwin") {
|
|
12
|
+
suffix = "dylib";
|
|
13
|
+
} else if (platform === "win32") {
|
|
14
|
+
suffix = "dll";
|
|
15
|
+
}
|
|
16
|
+
|
|
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}`);
|
|
20
|
+
|
|
21
|
+
// Load the library
|
|
22
|
+
// koffi.load returns an object where we can register functions
|
|
23
|
+
const lib = koffi.load(libPath);
|
|
24
|
+
|
|
25
|
+
// Define types if needed
|
|
26
|
+
// koffi handles void* as 'void*' or 'ptr'
|
|
27
|
+
|
|
28
|
+
export const symbols = {
|
|
29
|
+
zclient_new: lib.func("zclient_new", "void *", []),
|
|
30
|
+
zclient_free: lib.func("zclient_free", "void", ["void *"]),
|
|
31
|
+
zclient_get: lib.func("zclient_get", "void *", ["void *", "const char *"]),
|
|
32
|
+
zclient_post: lib.func("zclient_post", "void *", [
|
|
33
|
+
"void *",
|
|
34
|
+
"const char *",
|
|
35
|
+
"const char *",
|
|
36
|
+
]),
|
|
37
|
+
zclient_request: lib.func("zclient_request", "void *", [
|
|
38
|
+
"void *", // ptr
|
|
39
|
+
"const char *", // method
|
|
40
|
+
"const char *", // url
|
|
41
|
+
"const char *", // headers
|
|
42
|
+
"const char *", // body
|
|
43
|
+
"uint8_t", // max_redirects
|
|
44
|
+
"bool", // antibot
|
|
45
|
+
]),
|
|
46
|
+
zclient_prefetch: lib.func("zclient_prefetch", "void", [
|
|
47
|
+
"void *",
|
|
48
|
+
"const char *",
|
|
49
|
+
]),
|
|
50
|
+
zclient_response_status: lib.func("zclient_response_status", "uint16_t", [
|
|
51
|
+
"void *",
|
|
52
|
+
]),
|
|
53
|
+
zclient_response_body: lib.func("zclient_response_body", "void *", [
|
|
54
|
+
"void *",
|
|
55
|
+
]),
|
|
56
|
+
zclient_response_body_len: lib.func("zclient_response_body_len", "uint64_t", [
|
|
57
|
+
"void *",
|
|
58
|
+
]),
|
|
59
|
+
zclient_response_headers: lib.func("zclient_response_headers", "void *", [
|
|
60
|
+
"void *",
|
|
61
|
+
]),
|
|
62
|
+
zclient_response_headers_len: lib.func(
|
|
63
|
+
"zclient_response_headers_len",
|
|
64
|
+
"uint64_t",
|
|
65
|
+
["void *"]
|
|
66
|
+
),
|
|
67
|
+
zclient_response_parse_header_offsets: lib.func(
|
|
68
|
+
"zclient_response_parse_header_offsets",
|
|
69
|
+
"void *",
|
|
70
|
+
["void *"]
|
|
71
|
+
),
|
|
72
|
+
zclient_header_offsets_free: lib.func("zclient_header_offsets_free", "void", [
|
|
73
|
+
"void *",
|
|
74
|
+
]),
|
|
75
|
+
z_find_header_value: lib.func("z_find_header_value", "void *", [
|
|
76
|
+
"void *", // raw headers ptr
|
|
77
|
+
"uint64_t", // len
|
|
78
|
+
"const char *", // key
|
|
79
|
+
]),
|
|
80
|
+
zclient_header_value_free: lib.func("zclient_header_value_free", "void", [
|
|
81
|
+
"void *",
|
|
82
|
+
]),
|
|
83
|
+
zclient_response_free: lib.func("zclient_response_free", "void", ["void *"]),
|
|
84
|
+
zclient_set_benchmark_mode: lib.func("zclient_set_benchmark_mode", "void", [
|
|
85
|
+
"void *",
|
|
86
|
+
"bool",
|
|
87
|
+
]),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Export a wrapper that matches structure of bun:ffi lib
|
|
91
|
+
export const nativeLib = {
|
|
92
|
+
symbols,
|
|
93
|
+
};
|
package/components/types.ts
CHANGED
|
@@ -108,18 +108,32 @@ export interface CacheConfig {
|
|
|
108
108
|
maxSize?: number;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Configuration for automatic request retries */
|
|
112
|
+
export interface RetryConfig {
|
|
113
|
+
/** Number of retry attempts (default: 0) */
|
|
114
|
+
count?: number;
|
|
115
|
+
/** Initial delay in milliseconds before first retry (default: 100) */
|
|
116
|
+
delay?: number;
|
|
117
|
+
/** Backoff multiplier for subsequent retries (2 = double delay each time) (default: 2) */
|
|
118
|
+
backoff?: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
/** Options for URL endpoint requests */
|
|
112
122
|
export interface UrlRequestOptions {
|
|
113
123
|
/** Request headers */
|
|
114
124
|
headers?: Record<string, string>;
|
|
115
125
|
/** Path to append to the base URL */
|
|
116
126
|
path?: string;
|
|
127
|
+
/** Request body (for POST, PUT, PATCH, DELETE) - Auto-stringifies objects */
|
|
128
|
+
body?: string | object | null;
|
|
117
129
|
/** Maximum number of redirects to follow */
|
|
118
130
|
maxRedirects?: number;
|
|
119
131
|
/** Automatically parse response body */
|
|
120
132
|
responseType?: "json" | "text" | "arraybuffer" | "blob";
|
|
121
133
|
/** Enable anti-bot protection (using curl-impersonate) */
|
|
122
134
|
antibot?: boolean;
|
|
135
|
+
/** Retry configuration for this request */
|
|
136
|
+
retry?: number | RetryConfig;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/** Interface for a URL endpoint with HTTP method helpers */
|
|
@@ -134,42 +148,26 @@ export interface UrlEndpoint {
|
|
|
134
148
|
): Promise<string>;
|
|
135
149
|
|
|
136
150
|
/** POST request */
|
|
151
|
+
post<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
|
|
137
152
|
post<T = any>(
|
|
138
|
-
body?: string | null,
|
|
139
|
-
options?: UrlRequestOptions
|
|
140
|
-
): Promise<JirenResponse<T>>;
|
|
141
|
-
post<T = any>(
|
|
142
|
-
body?: string | null,
|
|
143
153
|
options?: UrlRequestOptions & { responseType: "json" }
|
|
144
154
|
): Promise<T>;
|
|
145
155
|
|
|
146
156
|
/** PUT request */
|
|
157
|
+
put<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
|
|
147
158
|
put<T = any>(
|
|
148
|
-
body?: string | null,
|
|
149
|
-
options?: UrlRequestOptions
|
|
150
|
-
): Promise<JirenResponse<T>>;
|
|
151
|
-
put<T = any>(
|
|
152
|
-
body?: string | null,
|
|
153
159
|
options?: UrlRequestOptions & { responseType: "json" }
|
|
154
160
|
): Promise<T>;
|
|
155
161
|
|
|
156
162
|
/** PATCH request */
|
|
163
|
+
patch<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
|
|
157
164
|
patch<T = any>(
|
|
158
|
-
body?: string | null,
|
|
159
|
-
options?: UrlRequestOptions
|
|
160
|
-
): Promise<JirenResponse<T>>;
|
|
161
|
-
patch<T = any>(
|
|
162
|
-
body?: string | null,
|
|
163
165
|
options?: UrlRequestOptions & { responseType: "json" }
|
|
164
166
|
): Promise<T>;
|
|
165
167
|
|
|
166
168
|
/** DELETE request */
|
|
169
|
+
delete<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
|
|
167
170
|
delete<T = any>(
|
|
168
|
-
body?: string | null,
|
|
169
|
-
options?: UrlRequestOptions
|
|
170
|
-
): Promise<JirenResponse<T>>;
|
|
171
|
-
delete<T = any>(
|
|
172
|
-
body?: string | null,
|
|
173
171
|
options?: UrlRequestOptions & { responseType: "json" }
|
|
174
172
|
): Promise<T>;
|
|
175
173
|
|
package/index-node.ts
CHANGED
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jiren",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"author": "",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -47,5 +47,8 @@
|
|
|
47
47
|
"default": "./index-node.ts"
|
|
48
48
|
},
|
|
49
49
|
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"koffi": "^2.14.1"
|
|
50
53
|
}
|
|
51
54
|
}
|