jiren 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +768 -0
  2. package/components/cache.ts +451 -0
  3. package/components/client-node-native.ts +1410 -0
  4. package/components/client.ts +1852 -0
  5. package/components/index.ts +37 -0
  6. package/components/metrics.ts +314 -0
  7. package/components/native-cache-node.ts +170 -0
  8. package/components/native-cache.ts +222 -0
  9. package/components/native-json.ts +195 -0
  10. package/components/native-node.ts +138 -0
  11. package/components/native.ts +418 -0
  12. package/components/persistent-worker.ts +67 -0
  13. package/components/subprocess-worker.ts +60 -0
  14. package/components/types.ts +317 -0
  15. package/components/worker-pool.ts +153 -0
  16. package/components/worker.ts +154 -0
  17. package/dist/components/cache.d.ts +32 -0
  18. package/dist/components/cache.d.ts.map +1 -0
  19. package/dist/components/cache.js +374 -0
  20. package/dist/components/cache.js.map +1 -0
  21. package/dist/components/client-node-native.d.ts +71 -0
  22. package/dist/components/client-node-native.d.ts.map +1 -0
  23. package/dist/components/client-node-native.js +1055 -0
  24. package/dist/components/client-node-native.js.map +1 -0
  25. package/dist/components/metrics.d.ts +14 -0
  26. package/dist/components/metrics.d.ts.map +1 -0
  27. package/dist/components/metrics.js +260 -0
  28. package/dist/components/metrics.js.map +1 -0
  29. package/dist/components/native-cache-node.d.ts +41 -0
  30. package/dist/components/native-cache-node.d.ts.map +1 -0
  31. package/dist/components/native-cache-node.js +133 -0
  32. package/dist/components/native-cache-node.js.map +1 -0
  33. package/dist/components/native-node.d.ts +82 -0
  34. package/dist/components/native-node.d.ts.map +1 -0
  35. package/dist/components/native-node.js +124 -0
  36. package/dist/components/native-node.js.map +1 -0
  37. package/dist/components/types.d.ts +248 -0
  38. package/dist/components/types.d.ts.map +1 -0
  39. package/dist/components/types.js +2 -0
  40. package/dist/components/types.js.map +1 -0
  41. package/dist/index-node.d.ts +3 -0
  42. package/dist/index-node.d.ts.map +1 -0
  43. package/dist/index-node.js +5 -0
  44. package/dist/index-node.js.map +1 -0
  45. package/index-node.ts +10 -0
  46. package/index.ts +9 -0
  47. package/lib/libcurl-impersonate.dylib +0 -0
  48. package/lib/libhttpclient.dylib +0 -0
  49. package/lib/libidn2.0.dylib +0 -0
  50. package/lib/libintl.8.dylib +0 -0
  51. package/lib/libunistring.5.dylib +0 -0
  52. package/lib/libzstd.1.5.7.dylib +0 -0
  53. package/package.json +62 -0
@@ -0,0 +1,1055 @@
1
+ import { nativeLib as lib } from "./native-node.js";
2
+ import { NativeCache } from "./native-cache-node.js";
3
+ import { MetricsCollector } from "./metrics.js";
4
+ import koffi from "koffi";
5
+ import zlib from "zlib";
6
+ const STATUS_TEXT = {
7
+ 200: "OK",
8
+ 201: "Created",
9
+ 204: "No Content",
10
+ 301: "Moved Permanently",
11
+ 302: "Found",
12
+ 400: "Bad Request",
13
+ 401: "Unauthorized",
14
+ 403: "Forbidden",
15
+ 404: "Not Found",
16
+ 500: "Internal Server Error",
17
+ 502: "Bad Gateway",
18
+ 503: "Service Unavailable",
19
+ };
20
+ export function defineUrls(urls) {
21
+ return urls;
22
+ }
23
+ const clientRegistry = new FinalizationRegistry((ptr) => {
24
+ try {
25
+ lib.symbols.zclient_free(ptr);
26
+ }
27
+ catch {
28
+ // Ignore cleanup errors
29
+ }
30
+ });
31
+ export class JirenClient {
32
+ ptr = null; // Koffi pointer
33
+ urlMap = new Map();
34
+ cacheConfig = new Map();
35
+ antibotConfig = new Map();
36
+ cache;
37
+ inflightRequests = new Map();
38
+ globalRetry;
39
+ requestInterceptors = [];
40
+ responseInterceptors = [];
41
+ errorInterceptors = [];
42
+ targetsPromise = null;
43
+ targetsComplete = new Set();
44
+ performanceMode = false;
45
+ useDefaultHeaders = true;
46
+ // Pre-computed headers
47
+ defaultHeadersStr;
48
+ defaultHeaders = {
49
+ "user-agent": "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",
50
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
51
+ "accept-encoding": "gzip",
52
+ "accept-language": "en-US,en;q=0.9",
53
+ "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
54
+ "sec-ch-ua-mobile": "?0",
55
+ "sec-ch-ua-platform": '"macOS"',
56
+ "sec-fetch-dest": "document",
57
+ "sec-fetch-mode": "navigate",
58
+ "sec-fetch-site": "none",
59
+ "sec-fetch-user": "?1",
60
+ "upgrade-insecure-requests": "1",
61
+ };
62
+ /** Type-safe URL accessor for warmed-up URLs */
63
+ url;
64
+ metricsCollector;
65
+ metrics;
66
+ constructor(options) {
67
+ this.ptr = lib.symbols.zclient_new();
68
+ if (!this.ptr)
69
+ throw new Error("Failed to create native client instance");
70
+ clientRegistry.register(this, this.ptr, this);
71
+ // Pre-computed default headers string
72
+ const orderedKeys = [
73
+ "sec-ch-ua",
74
+ "sec-ch-ua-mobile",
75
+ "sec-ch-ua-platform",
76
+ "upgrade-insecure-requests",
77
+ "user-agent",
78
+ "accept",
79
+ "sec-fetch-site",
80
+ "sec-fetch-mode",
81
+ "sec-fetch-user",
82
+ "sec-fetch-dest",
83
+ "accept-encoding",
84
+ "accept-language",
85
+ ];
86
+ this.defaultHeadersStr = orderedKeys
87
+ .map((k) => `${k}: ${this.defaultHeaders[k]}`)
88
+ .join("\r\n");
89
+ // Initialize native cache (faster than JS implementation)
90
+ this.cache = new NativeCache(100);
91
+ // Initialize metrics
92
+ this.metricsCollector = new MetricsCollector();
93
+ this.metrics = this.metricsCollector;
94
+ // Performance mode (default: true for maximum speed)
95
+ this.performanceMode = options?.performanceMode ?? true;
96
+ // Default headers (default: true)
97
+ this.useDefaultHeaders = options?.defaultHeaders ?? true;
98
+ // Enable benchmark mode if requested
99
+ if (options?.benchmark) {
100
+ lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
101
+ }
102
+ // Process target URLs
103
+ const targets = options?.urls ?? options?.targets;
104
+ if (targets) {
105
+ const urls = [];
106
+ if (Array.isArray(targets)) {
107
+ for (const item of targets) {
108
+ if (typeof item === "string") {
109
+ urls.push(item);
110
+ }
111
+ else {
112
+ const config = item;
113
+ urls.push(config.url);
114
+ this.urlMap.set(config.key, config.url);
115
+ if (config.cache) {
116
+ const cacheConfig = typeof config.cache === "boolean"
117
+ ? { enabled: true, ttl: 60000 }
118
+ : { enabled: true, ttl: config.cache.ttl || 60000 };
119
+ this.cacheConfig.set(config.key, cacheConfig);
120
+ }
121
+ if (config.antibot) {
122
+ this.antibotConfig.set(config.key, true);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ else {
128
+ for (const [key, urlConfig] of Object.entries(targets)) {
129
+ if (typeof urlConfig === "string") {
130
+ urls.push(urlConfig);
131
+ this.urlMap.set(key, urlConfig);
132
+ }
133
+ else {
134
+ urls.push(urlConfig.url);
135
+ this.urlMap.set(key, urlConfig.url);
136
+ if (urlConfig.cache) {
137
+ const cacheConfig = 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
+ if (urlConfig.antibot) {
143
+ this.antibotConfig.set(key, true);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ if (urls.length > 0 && options?.preconnect !== false) {
149
+ this.targetsPromise = this.preconnect(urls).then(() => {
150
+ urls.forEach((url) => this.targetsComplete.add(url));
151
+ this.targetsPromise = null;
152
+ });
153
+ }
154
+ // Preload L2 disk cache entries into L1 memory
155
+ for (const [key, config] of this.cacheConfig.entries()) {
156
+ if (config.enabled) {
157
+ const url = this.urlMap.get(key);
158
+ if (url) {
159
+ this.cache.preloadL1(url);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ // Create proxy for type-safe URL access
165
+ this.url = this.createUrlAccessor();
166
+ // Store global retry config
167
+ if (options?.retry) {
168
+ this.globalRetry =
169
+ typeof options.retry === "number"
170
+ ? { count: options.retry, delay: 100, backoff: 2 }
171
+ : options.retry;
172
+ }
173
+ // Initialize interceptors
174
+ if (options?.interceptors) {
175
+ this.requestInterceptors = options.interceptors.request || [];
176
+ this.responseInterceptors = options.interceptors.response || [];
177
+ this.errorInterceptors = options.interceptors.error || [];
178
+ }
179
+ }
180
+ async waitFor(ms) {
181
+ return new Promise((resolve) => setTimeout(resolve, ms));
182
+ }
183
+ async waitForTargets() {
184
+ if (this.targetsPromise)
185
+ await this.targetsPromise;
186
+ }
187
+ async waitForWarmup() {
188
+ return this.waitForTargets();
189
+ }
190
+ createUrlAccessor() {
191
+ const self = this;
192
+ return new Proxy({}, {
193
+ get(_target, prop) {
194
+ const baseUrl = self.urlMap.get(prop);
195
+ if (!baseUrl) {
196
+ throw new Error(`URL key "${prop}" not found. Available keys: ${Array.from(self.urlMap.keys()).join(", ")}`);
197
+ }
198
+ const buildUrl = (path) => path
199
+ ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
200
+ : baseUrl;
201
+ return {
202
+ get: async (options) => {
203
+ if (self.targetsPromise) {
204
+ await self.targetsPromise;
205
+ }
206
+ const cacheConfig = self.cacheConfig.get(prop);
207
+ const useAntibot = options?.antibot ?? self.antibotConfig.get(prop) ?? false;
208
+ // Fast path: no cache
209
+ if (!cacheConfig?.enabled && self.performanceMode) {
210
+ const { headers: preparedHeaders, serializedBody: bodyStr } = self.prepareBody(options?.body, options?.headers);
211
+ const retryConfig = options?.retry ?? self.globalRetry;
212
+ return self._request("GET", buildUrl(options?.path), bodyStr, {
213
+ headers: preparedHeaders,
214
+ // Pass through options like maxRedirects
215
+ maxRedirects: options?.maxRedirects,
216
+ responseType: options?.responseType,
217
+ antibot: useAntibot,
218
+ timeout: options?.timeout,
219
+ retry: retryConfig,
220
+ });
221
+ }
222
+ const startTime = performance.now();
223
+ // Try cache
224
+ if (cacheConfig?.enabled) {
225
+ const cached = self.cache.get(baseUrl, options?.path, options);
226
+ if (cached) {
227
+ const responseTimeMs = performance.now() - startTime;
228
+ const cacheLayer = responseTimeMs < 0.5 ? "l1" : "l2";
229
+ if (!self.performanceMode) {
230
+ self.metricsCollector.recordRequest(prop, {
231
+ startTime,
232
+ responseTimeMs,
233
+ status: cached.status,
234
+ success: cached.ok,
235
+ bytesSent: 0,
236
+ bytesReceived: 0,
237
+ cacheHit: true,
238
+ cacheLayer,
239
+ dedupeHit: false,
240
+ });
241
+ }
242
+ return cached;
243
+ }
244
+ }
245
+ // Deduplication
246
+ const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(options?.headers || {})}`;
247
+ if (self.inflightRequests.has(dedupKey)) {
248
+ const dedupeStart = performance.now();
249
+ const result = await self.inflightRequests.get(dedupKey);
250
+ const responseTimeMs = performance.now() - dedupeStart;
251
+ if (!self.performanceMode) {
252
+ self.metricsCollector.recordRequest(prop, {
253
+ startTime: dedupeStart,
254
+ responseTimeMs,
255
+ status: typeof result === "object" && "status" in result
256
+ ? result.status
257
+ : 200,
258
+ success: true,
259
+ bytesSent: 0,
260
+ bytesReceived: 0,
261
+ cacheHit: false,
262
+ dedupeHit: true,
263
+ });
264
+ }
265
+ return result;
266
+ }
267
+ const requestPromise = (async () => {
268
+ try {
269
+ const response = await self._request("GET", buildUrl(options?.path), null, {
270
+ headers: options?.headers,
271
+ maxRedirects: options?.maxRedirects,
272
+ responseType: options?.responseType,
273
+ antibot: useAntibot,
274
+ timeout: options?.timeout,
275
+ });
276
+ if (cacheConfig?.enabled &&
277
+ typeof response === "object" &&
278
+ "status" in response) {
279
+ self.cache.set(baseUrl, response, cacheConfig.ttl, options?.path, options);
280
+ }
281
+ const responseTimeMs = performance.now() - startTime;
282
+ if (!self.performanceMode) {
283
+ self.metricsCollector.recordRequest(prop, {
284
+ startTime,
285
+ responseTimeMs,
286
+ status: typeof response === "object" && "status" in response
287
+ ? response.status
288
+ : 200,
289
+ success: typeof response === "object" && "ok" in response
290
+ ? response.ok
291
+ : true,
292
+ bytesSent: options?.body
293
+ ? JSON.stringify(options.body).length
294
+ : 0,
295
+ bytesReceived: 0,
296
+ cacheHit: false,
297
+ dedupeHit: false,
298
+ });
299
+ }
300
+ return response;
301
+ }
302
+ catch (error) {
303
+ if (!self.performanceMode) {
304
+ const responseTimeMs = performance.now() - startTime;
305
+ self.metricsCollector.recordRequest(prop, {
306
+ startTime,
307
+ responseTimeMs,
308
+ status: 0,
309
+ success: false,
310
+ bytesSent: 0,
311
+ bytesReceived: 0,
312
+ cacheHit: false,
313
+ dedupeHit: false,
314
+ error: error instanceof Error ? error.message : String(error),
315
+ });
316
+ }
317
+ throw error;
318
+ }
319
+ finally {
320
+ self.inflightRequests.delete(dedupKey);
321
+ }
322
+ })();
323
+ self.inflightRequests.set(dedupKey, requestPromise);
324
+ return requestPromise;
325
+ },
326
+ post: async (body, options) => {
327
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
328
+ return self._request("POST", buildUrl(options?.path), serializedBody, {
329
+ headers: preparedHeaders,
330
+ maxRedirects: options?.maxRedirects,
331
+ responseType: options?.responseType,
332
+ antibot: options?.antibot,
333
+ timeout: options?.timeout,
334
+ });
335
+ },
336
+ put: async (body, options) => {
337
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
338
+ return self._request("PUT", buildUrl(options?.path), serializedBody, {
339
+ headers: preparedHeaders,
340
+ maxRedirects: options?.maxRedirects,
341
+ responseType: options?.responseType,
342
+ antibot: options?.antibot,
343
+ timeout: options?.timeout,
344
+ });
345
+ },
346
+ patch: async (body, options) => {
347
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
348
+ return self._request("PATCH", buildUrl(options?.path), serializedBody, {
349
+ headers: preparedHeaders,
350
+ maxRedirects: options?.maxRedirects,
351
+ responseType: options?.responseType,
352
+ antibot: options?.antibot,
353
+ timeout: options?.timeout,
354
+ });
355
+ },
356
+ delete: async (options) => {
357
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(options?.body, options?.headers);
358
+ return self._request("DELETE", buildUrl(options?.path), serializedBody, {
359
+ headers: preparedHeaders,
360
+ maxRedirects: options?.maxRedirects,
361
+ responseType: options?.responseType,
362
+ antibot: options?.antibot,
363
+ timeout: options?.timeout,
364
+ });
365
+ },
366
+ head: async (options) => {
367
+ return self._request("HEAD", buildUrl(options?.path), null, {
368
+ headers: options?.headers,
369
+ maxRedirects: options?.maxRedirects,
370
+ antibot: options?.antibot,
371
+ timeout: options?.timeout,
372
+ });
373
+ },
374
+ options: async (options) => {
375
+ return self._request("OPTIONS", buildUrl(options?.path), null, {
376
+ headers: options?.headers,
377
+ maxRedirects: options?.maxRedirects,
378
+ antibot: options?.antibot,
379
+ timeout: options?.timeout,
380
+ });
381
+ },
382
+ trace: async (options) => {
383
+ // Trace method
384
+ return self._request("TRACE", buildUrl(options?.path), null, {
385
+ headers: options?.headers,
386
+ maxRedirects: options?.maxRedirects,
387
+ antibot: options?.antibot,
388
+ timeout: options?.timeout,
389
+ });
390
+ },
391
+ prefetch: async (options) => {
392
+ self.cache.clear(baseUrl);
393
+ // Make fresh request to populate cache
394
+ const cacheConfig = self.cacheConfig.get(prop);
395
+ if (cacheConfig?.enabled) {
396
+ await self._request("GET", buildUrl(options?.path), null, {
397
+ headers: options?.headers,
398
+ maxRedirects: options?.maxRedirects,
399
+ antibot: options?.antibot,
400
+ timeout: options?.timeout,
401
+ });
402
+ }
403
+ },
404
+ getJsonFields: async (fields, options) => {
405
+ const { headers: preparedHeaders } = self.prepareBody(options?.body, options?.headers);
406
+ const response = (await self._request("GET", buildUrl(options?.path), null, {
407
+ headers: preparedHeaders,
408
+ maxRedirects: options?.maxRedirects,
409
+ antibot: options?.antibot,
410
+ timeout: options?.timeout,
411
+ retry: options?.retry ?? self.globalRetry,
412
+ }));
413
+ return response.body.jsonFields(fields);
414
+ },
415
+ /**
416
+ * Download with progress tracking
417
+ * Note: For Node.js, HTTPS uses fetch-based streaming. HTTP uses native streaming.
418
+ * @example
419
+ * const response = await client.url.cdn.download({
420
+ * path: '/large-file.zip',
421
+ * onDownloadProgress: (progress) => {
422
+ * console.log(`${progress.percent}%`);
423
+ * }
424
+ * });
425
+ */
426
+ download: async (options) => {
427
+ const url = buildUrl(options?.path);
428
+ // If no progress callback, use fast non-streaming path
429
+ if (!options?.onDownloadProgress) {
430
+ return self._request("GET", url, null, {
431
+ headers: options?.headers,
432
+ });
433
+ }
434
+ // For Node.js, use native fetch for HTTPS streaming
435
+ const isHttps = url.startsWith("https://");
436
+ if (isHttps) {
437
+ // Use Node.js fetch with streaming
438
+ const fetchOptions = {
439
+ method: "GET",
440
+ headers: options?.headers,
441
+ };
442
+ const response = await fetch(url, fetchOptions);
443
+ const contentLength = parseInt(response.headers.get("content-length") || "0", 10);
444
+ const reader = response.body?.getReader();
445
+ if (!reader) {
446
+ throw new Error("Failed to get response reader");
447
+ }
448
+ const chunks = [];
449
+ let loaded = 0;
450
+ const startTime = performance.now();
451
+ let lastLoaded = 0;
452
+ let lastTime = startTime;
453
+ while (true) {
454
+ const { done, value } = await reader.read();
455
+ if (done)
456
+ break;
457
+ chunks.push(value);
458
+ loaded += value.length;
459
+ // Calculate progress
460
+ const now = performance.now();
461
+ const elapsed = now - lastTime;
462
+ const bytesThisInterval = loaded - lastLoaded;
463
+ const speed = elapsed > 0 ? (bytesThisInterval / elapsed) * 1000 : 0;
464
+ const remaining = contentLength > 0 ? contentLength - loaded : 0;
465
+ const eta = speed > 0 ? (remaining / speed) * 1000 : 0;
466
+ options.onDownloadProgress({
467
+ loaded,
468
+ total: contentLength,
469
+ percent: contentLength > 0
470
+ ? Math.round((loaded / contentLength) * 100)
471
+ : 0,
472
+ speed: Math.round(speed),
473
+ eta: Math.round(eta),
474
+ });
475
+ lastLoaded = loaded;
476
+ lastTime = now;
477
+ }
478
+ // Final progress
479
+ options.onDownloadProgress({
480
+ loaded,
481
+ total: contentLength || loaded,
482
+ percent: 100,
483
+ speed: 0,
484
+ eta: 0,
485
+ });
486
+ // Combine chunks
487
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
488
+ const buffer = new Uint8Array(totalLength);
489
+ let offset = 0;
490
+ for (const chunk of chunks) {
491
+ buffer.set(chunk, offset);
492
+ offset += chunk.length;
493
+ }
494
+ // Parse headers
495
+ const headersObj = {};
496
+ response.headers.forEach((value, key) => {
497
+ headersObj[key.toLowerCase()] = value;
498
+ });
499
+ // Build response
500
+ const decoder = new TextDecoder();
501
+ let bodyUsed = false;
502
+ const bodyObj = {
503
+ bodyUsed: false,
504
+ arrayBuffer: async () => buffer.buffer,
505
+ blob: async () => new Blob([buffer]),
506
+ text: async () => {
507
+ bodyUsed = true;
508
+ return decoder.decode(buffer);
509
+ },
510
+ json: async () => {
511
+ bodyUsed = true;
512
+ return JSON.parse(decoder.decode(buffer));
513
+ },
514
+ jsonFields: async (fields) => {
515
+ bodyUsed = true;
516
+ const fullObj = JSON.parse(decoder.decode(buffer));
517
+ const result = {};
518
+ for (const field of fields) {
519
+ if (field in fullObj) {
520
+ result[field] = fullObj[field];
521
+ }
522
+ }
523
+ return result;
524
+ },
525
+ };
526
+ Object.defineProperty(bodyObj, "bodyUsed", {
527
+ get: () => bodyUsed,
528
+ });
529
+ return {
530
+ url,
531
+ status: response.status,
532
+ statusText: response.statusText,
533
+ headers: headersObj,
534
+ ok: response.ok,
535
+ redirected: response.redirected,
536
+ type: "basic",
537
+ body: bodyObj,
538
+ };
539
+ }
540
+ // HTTP: Fall back to regular request with final progress
541
+ const response = await self._request("GET", url, null, {
542
+ headers: options?.headers,
543
+ });
544
+ const bodyText = await response.body.text();
545
+ const bodySize = Buffer.byteLength(bodyText);
546
+ options.onDownloadProgress({
547
+ loaded: bodySize,
548
+ total: bodySize,
549
+ percent: 100,
550
+ speed: 0,
551
+ eta: 0,
552
+ });
553
+ return {
554
+ ...response,
555
+ body: {
556
+ ...response.body,
557
+ text: async () => bodyText,
558
+ json: async () => JSON.parse(bodyText),
559
+ arrayBuffer: async () => Buffer.from(bodyText).buffer,
560
+ blob: async () => new Blob([bodyText]),
561
+ bodyUsed: true,
562
+ jsonFields: async (fields) => {
563
+ const fullObj = JSON.parse(bodyText);
564
+ const result = {};
565
+ for (const field of fields) {
566
+ if (field in fullObj) {
567
+ result[field] = fullObj[field];
568
+ }
569
+ }
570
+ return result;
571
+ },
572
+ },
573
+ };
574
+ },
575
+ /**
576
+ * Upload with progress tracking
577
+ * @param options - Request options with onUploadProgress callback
578
+ * @example
579
+ * await client.url.api.upload({
580
+ * method: 'POST',
581
+ * path: '/upload',
582
+ * body: largeData,
583
+ * onUploadProgress: (progress) => {
584
+ * console.log(`${progress.percent}%`);
585
+ * }
586
+ * });
587
+ */
588
+ upload: async (options) => {
589
+ const url = buildUrl(options?.path);
590
+ const method = options?.method || "POST";
591
+ // Prepare body
592
+ let bodyStr = null;
593
+ if (options?.body) {
594
+ bodyStr =
595
+ typeof options.body === "string"
596
+ ? options.body
597
+ : JSON.stringify(options.body);
598
+ }
599
+ const bodyLength = bodyStr
600
+ ? Buffer.byteLength(bodyStr, "utf-8")
601
+ : 0;
602
+ // If no progress callback or no body, use fast path
603
+ if (!options?.onUploadProgress || !bodyStr) {
604
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(options?.body, options?.headers);
605
+ return self._request(method, url, serializedBody, {
606
+ headers: preparedHeaders,
607
+ });
608
+ }
609
+ // Fire initial progress event (0%)
610
+ const startTime = performance.now();
611
+ options.onUploadProgress({
612
+ loaded: 0,
613
+ total: bodyLength,
614
+ percent: 0,
615
+ speed: 0,
616
+ eta: 0,
617
+ });
618
+ // Prepare headers
619
+ const { headers: preparedHeaders } = self.prepareBody(options.body, options.headers);
620
+ // For Node.js, use fetch with manual progress tracking
621
+ // Since fetch doesn't support upload progress natively, we simulate it
622
+ const response = await fetch(url, {
623
+ method,
624
+ headers: preparedHeaders,
625
+ body: bodyStr,
626
+ });
627
+ // Fire final progress (100%)
628
+ const elapsed = performance.now() - startTime;
629
+ const speed = elapsed > 0 ? (bodyLength / elapsed) * 1000 : 0;
630
+ options.onUploadProgress({
631
+ loaded: bodyLength,
632
+ total: bodyLength,
633
+ percent: 100,
634
+ speed: Math.round(speed),
635
+ eta: 0,
636
+ });
637
+ // Parse response
638
+ const headersObj = {};
639
+ response.headers.forEach((value, key) => {
640
+ headersObj[key.toLowerCase()] = value;
641
+ });
642
+ const responseBody = await response.text();
643
+ let bodyUsed = false;
644
+ const bodyObj = {
645
+ bodyUsed: false,
646
+ text: async () => {
647
+ bodyUsed = true;
648
+ return responseBody;
649
+ },
650
+ json: async () => {
651
+ bodyUsed = true;
652
+ return JSON.parse(responseBody);
653
+ },
654
+ jsonFields: async (fields) => {
655
+ bodyUsed = true;
656
+ const fullObj = JSON.parse(responseBody);
657
+ const result = {};
658
+ for (const field of fields) {
659
+ if (field in fullObj) {
660
+ result[field] = fullObj[field];
661
+ }
662
+ }
663
+ return result;
664
+ },
665
+ arrayBuffer: async () => {
666
+ bodyUsed = true;
667
+ return Buffer.from(responseBody).buffer;
668
+ },
669
+ blob: async () => {
670
+ bodyUsed = true;
671
+ return new Blob([responseBody]);
672
+ },
673
+ };
674
+ Object.defineProperty(bodyObj, "bodyUsed", {
675
+ get: () => bodyUsed,
676
+ });
677
+ return {
678
+ url,
679
+ status: response.status,
680
+ statusText: response.statusText,
681
+ headers: headersObj,
682
+ ok: response.ok,
683
+ redirected: response.redirected,
684
+ type: "basic",
685
+ body: bodyObj,
686
+ };
687
+ },
688
+ };
689
+ },
690
+ });
691
+ }
692
+ close() {
693
+ if (this.ptr) {
694
+ // Unregister from FinalizationRegistry since we're manually closing
695
+ clientRegistry.unregister(this);
696
+ lib.symbols.zclient_free(this.ptr);
697
+ this.ptr = null;
698
+ }
699
+ // Close native cache
700
+ this.cache.close();
701
+ }
702
+ [Symbol.dispose]() {
703
+ this.close();
704
+ }
705
+ use(interceptors) {
706
+ if (interceptors.request)
707
+ this.requestInterceptors.push(...interceptors.request);
708
+ if (interceptors.response)
709
+ this.responseInterceptors.push(...interceptors.response);
710
+ if (interceptors.error)
711
+ this.errorInterceptors.push(...interceptors.error);
712
+ return this;
713
+ }
714
+ async preconnect(urls) {
715
+ if (!this.ptr)
716
+ throw new Error("Client is closed");
717
+ await Promise.all(urls.map((url) => new Promise((resolve) => {
718
+ lib.symbols.zclient_prefetch(this.ptr, url);
719
+ resolve();
720
+ })));
721
+ }
722
+ async warmup(urls) {
723
+ return this.preconnect(urls);
724
+ }
725
+ prefetch(urls) {
726
+ this.preconnect(urls);
727
+ }
728
+ async _request(method, url, body, options) {
729
+ if (!this.ptr)
730
+ throw new Error("Client is closed");
731
+ // Normalize options
732
+ let headers = {};
733
+ let maxRedirects = 5;
734
+ let responseType;
735
+ let antibot = false;
736
+ let timeout;
737
+ if (options) {
738
+ if ("maxRedirects" in options ||
739
+ "headers" in options ||
740
+ "responseType" in options ||
741
+ "method" in options ||
742
+ "timeout" in options ||
743
+ "antibot" in options) {
744
+ const opts = options;
745
+ if (opts.headers)
746
+ headers = opts.headers;
747
+ if (opts.maxRedirects !== undefined)
748
+ maxRedirects = opts.maxRedirects;
749
+ if (opts.responseType)
750
+ responseType = opts.responseType;
751
+ if (opts.antibot !== undefined)
752
+ antibot = opts.antibot;
753
+ if (opts.timeout !== undefined)
754
+ timeout = opts.timeout;
755
+ }
756
+ else {
757
+ headers = options;
758
+ }
759
+ }
760
+ // Run interceptors
761
+ let ctx = { method, url, headers, body };
762
+ if (this.requestInterceptors.length > 0) {
763
+ for (const interceptor of this.requestInterceptors) {
764
+ ctx = await interceptor(ctx);
765
+ }
766
+ method = ctx.method;
767
+ url = ctx.url;
768
+ headers = ctx.headers;
769
+ body = ctx.body ?? null;
770
+ }
771
+ // Prepare headers
772
+ let headerStr;
773
+ const hasCustomHeaders = Object.keys(headers).length > 0;
774
+ if (hasCustomHeaders) {
775
+ const finalHeaders = this.useDefaultHeaders
776
+ ? { ...this.defaultHeaders, ...headers }
777
+ : headers;
778
+ const orderedHeaders = {};
779
+ const keys = [
780
+ "sec-ch-ua",
781
+ "sec-ch-ua-mobile",
782
+ "sec-ch-ua-platform",
783
+ "upgrade-insecure-requests",
784
+ "user-agent",
785
+ "accept",
786
+ "sec-fetch-site",
787
+ "sec-fetch-mode",
788
+ "sec-fetch-user",
789
+ "sec-fetch-dest",
790
+ "accept-encoding",
791
+ "accept-language",
792
+ ];
793
+ for (const key of keys) {
794
+ if (finalHeaders[key]) {
795
+ orderedHeaders[key] = finalHeaders[key];
796
+ delete finalHeaders[key];
797
+ }
798
+ }
799
+ for (const [key, value] of Object.entries(finalHeaders)) {
800
+ orderedHeaders[key] = value;
801
+ }
802
+ headerStr = Object.entries(orderedHeaders)
803
+ .map(([k, v]) => `${k.toLowerCase()}: ${v}`)
804
+ .join("\r\n");
805
+ }
806
+ else {
807
+ headerStr = this.useDefaultHeaders ? this.defaultHeadersStr : "";
808
+ }
809
+ // Retry logic
810
+ let retryConfig = this.globalRetry;
811
+ if (options && typeof options === "object" && "retry" in options) {
812
+ const userRetry = options.retry;
813
+ if (typeof userRetry === "number") {
814
+ retryConfig = { count: userRetry, delay: 100, backoff: 2 };
815
+ }
816
+ else if (typeof userRetry === "object") {
817
+ retryConfig = userRetry;
818
+ }
819
+ }
820
+ let attempts = 0;
821
+ const maxAttempts = (retryConfig?.count || 0) + 1;
822
+ let currentDelay = retryConfig?.delay || 100;
823
+ const backoff = retryConfig?.backoff || 2;
824
+ let lastError;
825
+ while (attempts < maxAttempts) {
826
+ attempts++;
827
+ try {
828
+ // Create the request promise
829
+ const makeRequest = async () => {
830
+ const respPtr = lib.symbols.zclient_request(this.ptr, method, url, headerStr.length > 0 ? headerStr : null, body || null, maxRedirects, antibot);
831
+ if (!respPtr) {
832
+ throw new Error("Native request failed (returned null pointer)");
833
+ }
834
+ const response = this.parseResponse(respPtr, url);
835
+ // Run response interceptors
836
+ let finalResponse = response;
837
+ if (this.responseInterceptors.length > 0) {
838
+ let responseCtx = {
839
+ request: ctx,
840
+ response,
841
+ };
842
+ for (const interceptor of this.responseInterceptors) {
843
+ responseCtx = await interceptor(responseCtx);
844
+ }
845
+ finalResponse = responseCtx.response;
846
+ }
847
+ if (responseType) {
848
+ if (responseType === "json")
849
+ return finalResponse.body.json();
850
+ if (responseType === "text")
851
+ return finalResponse.body.text();
852
+ if (responseType === "arraybuffer")
853
+ return finalResponse.body.arrayBuffer();
854
+ if (responseType === "blob")
855
+ return finalResponse.body.blob();
856
+ }
857
+ return finalResponse;
858
+ };
859
+ // Apply timeout if specified
860
+ if (timeout && timeout > 0) {
861
+ const timeoutPromise = new Promise((_, reject) => {
862
+ setTimeout(() => {
863
+ const error = new Error(`Request timeout after ${timeout}ms`);
864
+ error.name = "TimeoutError";
865
+ reject(error);
866
+ }, timeout);
867
+ });
868
+ return await Promise.race([makeRequest(), timeoutPromise]);
869
+ }
870
+ return await makeRequest();
871
+ }
872
+ catch (err) {
873
+ if (this.errorInterceptors.length > 0) {
874
+ for (const interceptor of this.errorInterceptors) {
875
+ await interceptor(err, ctx);
876
+ }
877
+ }
878
+ lastError = err;
879
+ // Don't retry on timeout errors
880
+ if (err.name === "TimeoutError") {
881
+ throw err;
882
+ }
883
+ if (attempts < maxAttempts) {
884
+ await this.waitFor(currentDelay);
885
+ currentDelay *= backoff;
886
+ }
887
+ }
888
+ }
889
+ throw lastError || new Error("Request failed after retries");
890
+ }
891
+ parseResponse(respPtr, url) {
892
+ if (!respPtr)
893
+ throw new Error("Native request failed (returned null pointer)");
894
+ try {
895
+ const status = lib.symbols.zclient_response_status(respPtr);
896
+ const len = Number(lib.symbols.zclient_response_body_len(respPtr));
897
+ const bodyPtr = lib.symbols.zclient_response_body(respPtr);
898
+ const headersLen = Number(lib.symbols.zclient_response_headers_len(respPtr));
899
+ let headersObj = {};
900
+ if (headersLen > 0) {
901
+ const rawHeadersPtr = lib.symbols.zclient_response_headers(respPtr);
902
+ if (rawHeadersPtr) {
903
+ const raw = Buffer.from(koffi.decode(rawHeadersPtr, "uint8_t", headersLen));
904
+ headersObj = new NativeHeaders(raw);
905
+ }
906
+ }
907
+ const headersProxy = new Proxy(headersObj instanceof NativeHeaders ? headersObj : {}, {
908
+ get(target, prop) {
909
+ if (target instanceof NativeHeaders && typeof prop === "string") {
910
+ if (prop === "toJSON")
911
+ return () => target.toJSON();
912
+ const val = target.get(prop);
913
+ if (val !== null)
914
+ return val;
915
+ }
916
+ return Reflect.get(target, prop);
917
+ },
918
+ });
919
+ let buffer = Buffer.alloc(0);
920
+ if (len > 0 && bodyPtr) {
921
+ buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
922
+ // Handle GZIP
923
+ const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
924
+ let gzipOffset = -1;
925
+ for (let i = 0; i < Math.min(16, buffer.length - 1); i++) {
926
+ if (buffer[i] === 0x1f && buffer[i + 1] === 0x8b) {
927
+ gzipOffset = i;
928
+ break;
929
+ }
930
+ }
931
+ if (contentEncoding === "gzip" || gzipOffset >= 0) {
932
+ try {
933
+ const gzipData = gzipOffset > 0 ? buffer.slice(gzipOffset) : buffer;
934
+ buffer = zlib.gunzipSync(gzipData);
935
+ }
936
+ catch (e) {
937
+ // Keep original buffer
938
+ }
939
+ }
940
+ }
941
+ let bodyUsed = false;
942
+ const consumeBody = () => {
943
+ bodyUsed = true;
944
+ };
945
+ const bodyObj = {
946
+ bodyUsed: false,
947
+ arrayBuffer: async () => {
948
+ consumeBody();
949
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
950
+ },
951
+ blob: async () => {
952
+ consumeBody();
953
+ return new Blob([buffer]);
954
+ },
955
+ text: async () => {
956
+ consumeBody();
957
+ return buffer.toString("utf-8");
958
+ },
959
+ json: async () => {
960
+ consumeBody();
961
+ return JSON.parse(buffer.toString("utf-8"));
962
+ },
963
+ jsonFields: async (fields) => {
964
+ consumeBody();
965
+ const fullObj = JSON.parse(buffer.toString("utf-8"));
966
+ const result = {};
967
+ for (const field of fields) {
968
+ if (field in fullObj) {
969
+ result[field] = fullObj[field];
970
+ }
971
+ }
972
+ return result;
973
+ },
974
+ };
975
+ Object.defineProperty(bodyObj, "bodyUsed", {
976
+ get: () => bodyUsed,
977
+ });
978
+ return {
979
+ url,
980
+ status,
981
+ statusText: STATUS_TEXT[status] || "",
982
+ headers: headersProxy,
983
+ ok: status >= 200 && status < 300,
984
+ redirected: false,
985
+ type: "basic",
986
+ body: bodyObj,
987
+ };
988
+ }
989
+ finally {
990
+ lib.symbols.zclient_response_free(respPtr);
991
+ }
992
+ }
993
+ /**
994
+ * Helper to prepare body and headers for requests.
995
+ */
996
+ prepareBody(body, userHeaders) {
997
+ let serializedBody = null;
998
+ const headers = { ...userHeaders };
999
+ if (body !== null && body !== undefined) {
1000
+ if (typeof body === "object") {
1001
+ serializedBody = JSON.stringify(body);
1002
+ const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
1003
+ if (!hasContentType) {
1004
+ headers["Content-Type"] = "application/json";
1005
+ }
1006
+ }
1007
+ else {
1008
+ serializedBody = String(body);
1009
+ }
1010
+ }
1011
+ return { headers, serializedBody };
1012
+ }
1013
+ }
1014
+ class NativeHeaders {
1015
+ raw;
1016
+ len;
1017
+ cache = new Map();
1018
+ parsed = false;
1019
+ constructor(raw) {
1020
+ this.raw = raw;
1021
+ this.len = raw.length;
1022
+ }
1023
+ ensureParsed() {
1024
+ if (this.parsed)
1025
+ return;
1026
+ try {
1027
+ const text = this.raw.toString("utf-8");
1028
+ const lines = text.split("\r\n");
1029
+ for (const line of lines) {
1030
+ if (!line)
1031
+ continue;
1032
+ const colon = line.indexOf(":");
1033
+ if (colon === -1)
1034
+ continue;
1035
+ const key = line.substring(0, colon).trim().toLowerCase();
1036
+ const val = line.substring(colon + 1).trim();
1037
+ this.cache.set(key, val);
1038
+ }
1039
+ }
1040
+ catch (e) {
1041
+ // Ignore parsing errors
1042
+ }
1043
+ this.parsed = true;
1044
+ }
1045
+ get(name) {
1046
+ const target = name.toLowerCase();
1047
+ this.ensureParsed();
1048
+ return this.cache.get(target) || null;
1049
+ }
1050
+ toJSON() {
1051
+ this.ensureParsed();
1052
+ return Object.fromEntries(this.cache);
1053
+ }
1054
+ }
1055
+ //# sourceMappingURL=client-node-native.js.map