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