jiren 3.3.0 → 3.5.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.
@@ -0,0 +1,652 @@
1
+ import { MetricsCollector } from "./metrics.js";
2
+ const STATUS_TEXT = {
3
+ 200: "OK",
4
+ 201: "Created",
5
+ 204: "No Content",
6
+ 301: "Moved Permanently",
7
+ 302: "Found",
8
+ 400: "Bad Request",
9
+ 401: "Unauthorized",
10
+ 403: "Forbidden",
11
+ 404: "Not Found",
12
+ 500: "Internal Server Error",
13
+ 502: "Bad Gateway",
14
+ 503: "Service Unavailable",
15
+ };
16
+ const RAW_BODY = Symbol("jiren.raw.body");
17
+ let didWarnFallback = false;
18
+ export function defineUrls(urls) {
19
+ return urls;
20
+ }
21
+ export class JirenClient {
22
+ closed = false;
23
+ urlMap = new Map();
24
+ cacheConfig = new Map();
25
+ antibotConfig = new Map();
26
+ inflightRequests = new Map();
27
+ globalRetry;
28
+ requestInterceptors = [];
29
+ responseInterceptors = [];
30
+ errorInterceptors = [];
31
+ targetsPromise = null;
32
+ targetsComplete = new Set();
33
+ performanceMode = false;
34
+ useDefaultHeaders = true;
35
+ cacheStore = new Map();
36
+ defaultHeaders = {
37
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
38
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
39
+ "accept-encoding": "gzip",
40
+ "accept-language": "en-US,en;q=0.9",
41
+ "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
42
+ "sec-ch-ua-mobile": "?0",
43
+ "sec-ch-ua-platform": '"Windows"',
44
+ "sec-fetch-dest": "document",
45
+ "sec-fetch-mode": "navigate",
46
+ "sec-fetch-site": "none",
47
+ "sec-fetch-user": "?1",
48
+ "upgrade-insecure-requests": "1",
49
+ };
50
+ url;
51
+ metricsCollector;
52
+ metrics;
53
+ constructor(options) {
54
+ if (!didWarnFallback) {
55
+ didWarnFallback = true;
56
+ console.warn("[Jiren] Using fetch fallback mode (reduced performance/features).");
57
+ }
58
+ this.metricsCollector = new MetricsCollector();
59
+ this.metrics = this.metricsCollector;
60
+ this.performanceMode = options?.performanceMode ?? false;
61
+ this.useDefaultHeaders = options?.defaultHeaders ?? true;
62
+ const targets = options?.urls;
63
+ if (targets) {
64
+ const urls = [];
65
+ if (Array.isArray(targets)) {
66
+ for (const item of targets) {
67
+ if (typeof item === "string") {
68
+ urls.push(item);
69
+ }
70
+ else {
71
+ const config = item;
72
+ urls.push(config.url);
73
+ this.urlMap.set(config.key, config.url);
74
+ if (config.cache) {
75
+ const cacheConfig = typeof config.cache === "boolean"
76
+ ? { enabled: true, ttl: 60000 }
77
+ : { enabled: true, ttl: config.cache.ttl || 60000 };
78
+ this.cacheConfig.set(config.key, cacheConfig);
79
+ }
80
+ if (config.antibot) {
81
+ this.antibotConfig.set(config.key, true);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ else {
87
+ for (const [key, urlConfig] of Object.entries(targets)) {
88
+ if (typeof urlConfig === "string") {
89
+ urls.push(urlConfig);
90
+ this.urlMap.set(key, urlConfig);
91
+ }
92
+ else {
93
+ urls.push(urlConfig.url);
94
+ this.urlMap.set(key, urlConfig.url);
95
+ if (urlConfig.cache) {
96
+ const cacheConfig = typeof urlConfig.cache === "boolean"
97
+ ? { enabled: true, ttl: 60000 }
98
+ : { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
99
+ this.cacheConfig.set(key, cacheConfig);
100
+ }
101
+ if (urlConfig.antibot) {
102
+ this.antibotConfig.set(key, true);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ if (urls.length > 0 && options?.preconnect !== false) {
108
+ this.targetsPromise = this.preconnect(urls).then(() => {
109
+ urls.forEach((url) => this.targetsComplete.add(url));
110
+ this.targetsPromise = null;
111
+ });
112
+ }
113
+ }
114
+ this.url = this.createUrlAccessor();
115
+ if (options?.retry) {
116
+ this.globalRetry =
117
+ typeof options.retry === "number"
118
+ ? { count: options.retry, delay: 100, backoff: 2 }
119
+ : options.retry;
120
+ }
121
+ if (options?.interceptors) {
122
+ this.requestInterceptors = options.interceptors.request || [];
123
+ this.responseInterceptors = options.interceptors.response || [];
124
+ this.errorInterceptors = options.interceptors.error || [];
125
+ }
126
+ }
127
+ async waitFor(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
130
+ async waitForTargets() {
131
+ if (this.targetsPromise)
132
+ await this.targetsPromise;
133
+ }
134
+ async waitForWarmup() {
135
+ return this.waitForTargets();
136
+ }
137
+ createUrlAccessor() {
138
+ const self = this;
139
+ return new Proxy({}, {
140
+ get(_target, prop) {
141
+ const baseUrl = self.urlMap.get(prop);
142
+ if (!baseUrl) {
143
+ throw new Error(`URL key "${prop}" not found. Available keys: ${Array.from(self.urlMap.keys()).join(", ")}`);
144
+ }
145
+ const buildUrl = (path) => path
146
+ ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
147
+ : baseUrl;
148
+ return {
149
+ get: async (options) => {
150
+ if (self.targetsPromise) {
151
+ await self.targetsPromise;
152
+ }
153
+ const cacheConfig = self.cacheConfig.get(prop);
154
+ const useAntibot = options?.antibot ?? self.antibotConfig.get(prop) ?? false;
155
+ const url = buildUrl(options?.path);
156
+ const requestKey = self.getRequestKey("GET", url, options?.headers, null);
157
+ if (cacheConfig?.enabled) {
158
+ const cached = self.getCachedResponse(requestKey);
159
+ if (cached)
160
+ return cached;
161
+ }
162
+ if (self.inflightRequests.has(requestKey)) {
163
+ return self.inflightRequests.get(requestKey);
164
+ }
165
+ const startTime = performance.now();
166
+ const requestPromise = (async () => {
167
+ try {
168
+ const response = await self._request("GET", url, null, {
169
+ headers: options?.headers,
170
+ maxRedirects: options?.maxRedirects,
171
+ responseType: options?.responseType,
172
+ antibot: useAntibot,
173
+ timeout: options?.timeout,
174
+ retry: options?.retry,
175
+ });
176
+ if (cacheConfig?.enabled &&
177
+ self.isJirenResponse(response) &&
178
+ !options?.responseType) {
179
+ self.setCachedResponse(requestKey, response, cacheConfig.ttl);
180
+ }
181
+ if (!self.performanceMode && self.isJirenResponse(response)) {
182
+ const raw = response[RAW_BODY];
183
+ self.metricsCollector.recordRequest(prop, {
184
+ startTime,
185
+ responseTimeMs: performance.now() - startTime,
186
+ status: response.status,
187
+ success: response.ok,
188
+ bytesSent: 0,
189
+ bytesReceived: raw.length,
190
+ cacheHit: false,
191
+ dedupeHit: false,
192
+ });
193
+ }
194
+ return response;
195
+ }
196
+ catch (error) {
197
+ if (!self.performanceMode) {
198
+ self.metricsCollector.recordRequest(prop, {
199
+ startTime,
200
+ responseTimeMs: performance.now() - startTime,
201
+ status: 0,
202
+ success: false,
203
+ bytesSent: 0,
204
+ bytesReceived: 0,
205
+ cacheHit: false,
206
+ dedupeHit: false,
207
+ error: error instanceof Error ? error.message : String(error),
208
+ });
209
+ }
210
+ throw error;
211
+ }
212
+ finally {
213
+ self.inflightRequests.delete(requestKey);
214
+ }
215
+ })();
216
+ self.inflightRequests.set(requestKey, requestPromise);
217
+ return requestPromise;
218
+ },
219
+ post: async (body, options) => {
220
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
221
+ return self._request("POST", buildUrl(options?.path), serializedBody, {
222
+ headers: preparedHeaders,
223
+ maxRedirects: options?.maxRedirects,
224
+ responseType: options?.responseType,
225
+ antibot: options?.antibot,
226
+ timeout: options?.timeout,
227
+ retry: options?.retry,
228
+ });
229
+ },
230
+ put: async (body, options) => {
231
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
232
+ return self._request("PUT", buildUrl(options?.path), serializedBody, {
233
+ headers: preparedHeaders,
234
+ maxRedirects: options?.maxRedirects,
235
+ responseType: options?.responseType,
236
+ antibot: options?.antibot,
237
+ timeout: options?.timeout,
238
+ retry: options?.retry,
239
+ });
240
+ },
241
+ patch: async (body, options) => {
242
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(body, options?.headers);
243
+ return self._request("PATCH", buildUrl(options?.path), serializedBody, {
244
+ headers: preparedHeaders,
245
+ maxRedirects: options?.maxRedirects,
246
+ responseType: options?.responseType,
247
+ antibot: options?.antibot,
248
+ timeout: options?.timeout,
249
+ retry: options?.retry,
250
+ });
251
+ },
252
+ delete: async (options) => {
253
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(options?.body, options?.headers);
254
+ return self._request("DELETE", buildUrl(options?.path), serializedBody, {
255
+ headers: preparedHeaders,
256
+ maxRedirects: options?.maxRedirects,
257
+ responseType: options?.responseType,
258
+ antibot: options?.antibot,
259
+ timeout: options?.timeout,
260
+ retry: options?.retry,
261
+ });
262
+ },
263
+ head: async (options) => {
264
+ return self._request("HEAD", buildUrl(options?.path), null, {
265
+ headers: options?.headers,
266
+ maxRedirects: options?.maxRedirects,
267
+ antibot: options?.antibot,
268
+ timeout: options?.timeout,
269
+ retry: options?.retry,
270
+ });
271
+ },
272
+ options: async (options) => {
273
+ return self._request("OPTIONS", buildUrl(options?.path), null, {
274
+ headers: options?.headers,
275
+ maxRedirects: options?.maxRedirects,
276
+ antibot: options?.antibot,
277
+ timeout: options?.timeout,
278
+ retry: options?.retry,
279
+ });
280
+ },
281
+ trace: async (options) => {
282
+ return self._request("TRACE", buildUrl(options?.path), null, {
283
+ headers: options?.headers,
284
+ maxRedirects: options?.maxRedirects,
285
+ antibot: options?.antibot,
286
+ timeout: options?.timeout,
287
+ retry: options?.retry,
288
+ });
289
+ },
290
+ prefetch: async (options) => {
291
+ const targetUrl = buildUrl(options?.path);
292
+ for (const key of self.cacheStore.keys()) {
293
+ if (key.includes(targetUrl)) {
294
+ self.cacheStore.delete(key);
295
+ }
296
+ }
297
+ await self._request("GET", targetUrl, null, {
298
+ headers: options?.headers,
299
+ maxRedirects: options?.maxRedirects,
300
+ antibot: options?.antibot,
301
+ timeout: options?.timeout,
302
+ retry: options?.retry,
303
+ });
304
+ },
305
+ download: async (options) => {
306
+ const response = (await self._request("GET", buildUrl(options?.path), null, { headers: options?.headers }));
307
+ const total = response[RAW_BODY].length;
308
+ options?.onDownloadProgress?.({
309
+ loaded: total,
310
+ total,
311
+ percent: 100,
312
+ speed: 0,
313
+ eta: 0,
314
+ });
315
+ return self.cloneResponse(response);
316
+ },
317
+ upload: async (options) => {
318
+ const method = options?.method || "POST";
319
+ const { headers: preparedHeaders, serializedBody } = self.prepareBody(options?.body, options?.headers);
320
+ const total = serializedBody
321
+ ? Buffer.byteLength(serializedBody, "utf-8")
322
+ : 0;
323
+ if (options?.onUploadProgress) {
324
+ options.onUploadProgress({
325
+ loaded: 0,
326
+ total,
327
+ percent: 0,
328
+ speed: 0,
329
+ eta: 0,
330
+ });
331
+ }
332
+ const response = (await self._request(method, buildUrl(options?.path), serializedBody, { headers: preparedHeaders }));
333
+ if (options?.onUploadProgress) {
334
+ options.onUploadProgress({
335
+ loaded: total,
336
+ total,
337
+ percent: 100,
338
+ speed: 0,
339
+ eta: 0,
340
+ });
341
+ }
342
+ return self.cloneResponse(response);
343
+ },
344
+ getJsonFields: async (fields, options) => {
345
+ const json = await self._request("GET", buildUrl(options?.path), null, {
346
+ headers: options?.headers,
347
+ maxRedirects: options?.maxRedirects,
348
+ timeout: options?.timeout,
349
+ responseType: "json",
350
+ });
351
+ const result = {};
352
+ for (const field of fields) {
353
+ result[field] = json?.[field];
354
+ }
355
+ return result;
356
+ },
357
+ };
358
+ },
359
+ });
360
+ }
361
+ close() {
362
+ this.closed = true;
363
+ this.inflightRequests.clear();
364
+ this.cacheStore.clear();
365
+ }
366
+ [Symbol.dispose]() {
367
+ this.close();
368
+ }
369
+ use(interceptors) {
370
+ if (interceptors.request)
371
+ this.requestInterceptors.push(...interceptors.request);
372
+ if (interceptors.response)
373
+ this.responseInterceptors.push(...interceptors.response);
374
+ if (interceptors.error)
375
+ this.errorInterceptors.push(...interceptors.error);
376
+ return this;
377
+ }
378
+ async preconnect(urls) {
379
+ urls.forEach((url) => this.targetsComplete.add(url));
380
+ }
381
+ async warmup(urls) {
382
+ return this.preconnect(urls);
383
+ }
384
+ prefetch(urls) {
385
+ void this.preconnect(urls);
386
+ }
387
+ async _request(method, url, body, options) {
388
+ if (this.closed)
389
+ throw new Error("Client is closed");
390
+ let headers = {};
391
+ let responseType;
392
+ let timeout;
393
+ if (options) {
394
+ if ("maxRedirects" in options ||
395
+ "headers" in options ||
396
+ "responseType" in options ||
397
+ "method" in options ||
398
+ "timeout" in options ||
399
+ "antibot" in options) {
400
+ const opts = options;
401
+ if (opts.headers)
402
+ headers = opts.headers;
403
+ if (opts.responseType)
404
+ responseType = opts.responseType;
405
+ if (opts.timeout !== undefined)
406
+ timeout = opts.timeout;
407
+ }
408
+ else {
409
+ headers = options;
410
+ }
411
+ }
412
+ let ctx = { method, url, headers, body };
413
+ if (this.requestInterceptors.length > 0) {
414
+ for (const interceptor of this.requestInterceptors) {
415
+ ctx = await interceptor(ctx);
416
+ }
417
+ method = ctx.method;
418
+ url = ctx.url;
419
+ headers = ctx.headers;
420
+ body = ctx.body ?? null;
421
+ }
422
+ let retryConfig = this.globalRetry;
423
+ if (options && typeof options === "object" && "retry" in options) {
424
+ const userRetry = options?.retry;
425
+ if (typeof userRetry === "number") {
426
+ retryConfig = { count: userRetry, delay: 100, backoff: 2 };
427
+ }
428
+ else if (typeof userRetry === "object") {
429
+ retryConfig = userRetry;
430
+ }
431
+ }
432
+ const finalHeaders = this.buildHeaders(headers);
433
+ let attempts = 0;
434
+ const maxAttempts = (retryConfig?.count || 0) + 1;
435
+ let currentDelay = retryConfig?.delay || 100;
436
+ const backoff = retryConfig?.backoff || 2;
437
+ let lastError;
438
+ while (attempts < maxAttempts) {
439
+ attempts++;
440
+ try {
441
+ const controller = timeout && timeout > 0 ? new AbortController() : undefined;
442
+ let timeoutId;
443
+ if (controller && timeout && timeout > 0) {
444
+ timeoutId = setTimeout(() => controller.abort(), timeout);
445
+ }
446
+ try {
447
+ const response = await fetch(url, {
448
+ method,
449
+ headers: finalHeaders,
450
+ body: body || undefined,
451
+ redirect: "follow",
452
+ signal: controller?.signal,
453
+ });
454
+ const raw = Buffer.from(await response.arrayBuffer());
455
+ const parsed = this.createResponse(url, response, raw);
456
+ let finalResponse = parsed;
457
+ if (this.responseInterceptors.length > 0) {
458
+ let responseCtx = {
459
+ request: ctx,
460
+ response: parsed,
461
+ };
462
+ for (const interceptor of this.responseInterceptors) {
463
+ responseCtx = await interceptor(responseCtx);
464
+ }
465
+ finalResponse = responseCtx.response;
466
+ }
467
+ if (responseType === "json")
468
+ return finalResponse.body.json();
469
+ if (responseType === "text")
470
+ return finalResponse.body.text();
471
+ if (responseType === "arraybuffer")
472
+ return finalResponse.body.arrayBuffer();
473
+ if (responseType === "blob")
474
+ return finalResponse.body.blob();
475
+ return finalResponse;
476
+ }
477
+ catch (error) {
478
+ if (error.name === "AbortError" && timeout) {
479
+ const timeoutError = new Error(`Request timeout after ${timeout}ms`);
480
+ timeoutError.name = "TimeoutError";
481
+ throw timeoutError;
482
+ }
483
+ throw error;
484
+ }
485
+ finally {
486
+ if (timeoutId)
487
+ clearTimeout(timeoutId);
488
+ }
489
+ }
490
+ catch (err) {
491
+ if (this.errorInterceptors.length > 0) {
492
+ for (const interceptor of this.errorInterceptors) {
493
+ await interceptor(err, ctx);
494
+ }
495
+ }
496
+ lastError = err;
497
+ if (err.name === "TimeoutError") {
498
+ throw err;
499
+ }
500
+ if (attempts < maxAttempts) {
501
+ await this.waitFor(currentDelay);
502
+ currentDelay *= backoff;
503
+ }
504
+ }
505
+ }
506
+ throw lastError || new Error("Request failed after retries");
507
+ }
508
+ createResponse(url, response, buffer) {
509
+ const headers = {};
510
+ response.headers.forEach((value, key) => {
511
+ headers[key.toLowerCase()] = value;
512
+ });
513
+ let bodyUsed = false;
514
+ const bodyObj = {
515
+ bodyUsed: false,
516
+ arrayBuffer: async () => {
517
+ bodyUsed = true;
518
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
519
+ },
520
+ blob: async () => {
521
+ bodyUsed = true;
522
+ return new Blob([buffer]);
523
+ },
524
+ text: async () => {
525
+ bodyUsed = true;
526
+ return buffer.toString("utf-8");
527
+ },
528
+ json: async () => {
529
+ bodyUsed = true;
530
+ return JSON.parse(buffer.toString("utf-8"));
531
+ },
532
+ };
533
+ Object.defineProperty(bodyObj, "bodyUsed", {
534
+ get: () => bodyUsed,
535
+ });
536
+ const jirenResponse = {
537
+ url,
538
+ status: response.status,
539
+ statusText: response.statusText || STATUS_TEXT[response.status] || "",
540
+ headers,
541
+ ok: response.ok,
542
+ redirected: response.redirected,
543
+ type: response.type || "basic",
544
+ body: bodyObj,
545
+ [RAW_BODY]: buffer,
546
+ };
547
+ return jirenResponse;
548
+ }
549
+ cloneResponse(response) {
550
+ const clone = this.createBufferedResponse(response.url, response.status, response.statusText, response.headers, response.ok, response.redirected, response.type, response[RAW_BODY]);
551
+ return clone;
552
+ }
553
+ createBufferedResponse(url, status, statusText, headers, ok, redirected, type, buffer) {
554
+ let bodyUsed = false;
555
+ const bodyObj = {
556
+ bodyUsed: false,
557
+ arrayBuffer: async () => {
558
+ bodyUsed = true;
559
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
560
+ },
561
+ blob: async () => {
562
+ bodyUsed = true;
563
+ return new Blob([buffer]);
564
+ },
565
+ text: async () => {
566
+ bodyUsed = true;
567
+ return buffer.toString("utf-8");
568
+ },
569
+ json: async () => {
570
+ bodyUsed = true;
571
+ return JSON.parse(buffer.toString("utf-8"));
572
+ },
573
+ };
574
+ Object.defineProperty(bodyObj, "bodyUsed", {
575
+ get: () => bodyUsed,
576
+ });
577
+ return {
578
+ url,
579
+ status,
580
+ statusText,
581
+ headers: { ...headers },
582
+ ok,
583
+ redirected,
584
+ type,
585
+ body: bodyObj,
586
+ [RAW_BODY]: Buffer.from(buffer),
587
+ };
588
+ }
589
+ isJirenResponse(value) {
590
+ return (typeof value === "object" &&
591
+ value !== null &&
592
+ "status" in value &&
593
+ "body" in value);
594
+ }
595
+ prepareBody(body, userHeaders) {
596
+ let serializedBody = null;
597
+ const headers = { ...userHeaders };
598
+ if (body !== null && body !== undefined) {
599
+ if (typeof body === "object") {
600
+ serializedBody = JSON.stringify(body);
601
+ const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
602
+ if (!hasContentType) {
603
+ headers["Content-Type"] = "application/json";
604
+ }
605
+ }
606
+ else {
607
+ serializedBody = String(body);
608
+ }
609
+ }
610
+ return { headers, serializedBody };
611
+ }
612
+ buildHeaders(userHeaders) {
613
+ if (!this.useDefaultHeaders) {
614
+ return { ...userHeaders };
615
+ }
616
+ const normalized = {};
617
+ for (const [key, value] of Object.entries(this.defaultHeaders)) {
618
+ normalized[key.toLowerCase()] = value;
619
+ }
620
+ for (const [key, value] of Object.entries(userHeaders)) {
621
+ normalized[key.toLowerCase()] = value;
622
+ }
623
+ return normalized;
624
+ }
625
+ getRequestKey(method, url, headers, body) {
626
+ return `${method}:${url}:${JSON.stringify(headers || {})}:${body || ""}`;
627
+ }
628
+ getCachedResponse(key) {
629
+ const cached = this.cacheStore.get(key);
630
+ if (!cached)
631
+ return null;
632
+ if (Date.now() > cached.expiresAt) {
633
+ this.cacheStore.delete(key);
634
+ return null;
635
+ }
636
+ return this.createBufferedResponse(cached.url, cached.status, cached.statusText, cached.headers, cached.ok, cached.redirected, cached.type, cached.body);
637
+ }
638
+ setCachedResponse(key, response, ttl) {
639
+ this.cacheStore.set(key, {
640
+ status: response.status,
641
+ statusText: response.statusText,
642
+ headers: { ...response.headers },
643
+ url: response.url,
644
+ ok: response.ok,
645
+ redirected: response.redirected,
646
+ type: response.type,
647
+ body: Buffer.from(response[RAW_BODY]),
648
+ expiresAt: Date.now() + ttl,
649
+ });
650
+ }
651
+ }
652
+ //# sourceMappingURL=client-node-fetch.js.map