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.
- package/README.md +5 -2
- package/components/client-node-fetch.ts +953 -0
- package/components/client-node.ts +9 -0
- package/components/native-node.ts +120 -78
- package/dist/components/client-node-fetch.d.ts +56 -0
- package/dist/components/client-node-fetch.d.ts.map +1 -0
- package/dist/components/client-node-fetch.js +652 -0
- package/dist/components/client-node-fetch.js.map +1 -0
- package/dist/components/client-node.d.ts +3 -0
- package/dist/components/client-node.d.ts.map +1 -0
- package/dist/components/client-node.js +6 -0
- package/dist/components/client-node.js.map +1 -0
- package/dist/components/native-node.d.ts +155 -153
- package/dist/components/native-node.d.ts.map +1 -1
- package/dist/components/native-node.js +111 -78
- package/dist/components/native-node.js.map +1 -1
- package/dist/index-node.d.ts +1 -1
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +2 -2
- package/dist/index-node.js.map +1 -1
- package/index-node.ts +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
import { MetricsCollector } from "./metrics.js";
|
|
2
|
+
import type {
|
|
3
|
+
RequestOptions,
|
|
4
|
+
JirenResponse,
|
|
5
|
+
JirenResponseBody,
|
|
6
|
+
TargetUrlConfig,
|
|
7
|
+
UrlRequestOptions,
|
|
8
|
+
UrlEndpoint,
|
|
9
|
+
RetryConfig,
|
|
10
|
+
Interceptors,
|
|
11
|
+
RequestInterceptor,
|
|
12
|
+
ResponseInterceptor,
|
|
13
|
+
ErrorInterceptor,
|
|
14
|
+
InterceptorRequestContext,
|
|
15
|
+
InterceptorResponseContext,
|
|
16
|
+
MetricsAPI,
|
|
17
|
+
ProgressRequestOptions,
|
|
18
|
+
UrlConfig,
|
|
19
|
+
JirenClientOptions,
|
|
20
|
+
UrlAccessor,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
const STATUS_TEXT: Record<number, string> = {
|
|
24
|
+
200: "OK",
|
|
25
|
+
201: "Created",
|
|
26
|
+
204: "No Content",
|
|
27
|
+
301: "Moved Permanently",
|
|
28
|
+
302: "Found",
|
|
29
|
+
400: "Bad Request",
|
|
30
|
+
401: "Unauthorized",
|
|
31
|
+
403: "Forbidden",
|
|
32
|
+
404: "Not Found",
|
|
33
|
+
500: "Internal Server Error",
|
|
34
|
+
502: "Bad Gateway",
|
|
35
|
+
503: "Service Unavailable",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const RAW_BODY = Symbol("jiren.raw.body");
|
|
39
|
+
let didWarnFallback = false;
|
|
40
|
+
|
|
41
|
+
interface InternalResponse<T = any> extends JirenResponse<T> {
|
|
42
|
+
[RAW_BODY]: Buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CachedResponse {
|
|
46
|
+
status: number;
|
|
47
|
+
statusText: string;
|
|
48
|
+
headers: Record<string, string>;
|
|
49
|
+
url: string;
|
|
50
|
+
ok: boolean;
|
|
51
|
+
redirected: boolean;
|
|
52
|
+
type: "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect";
|
|
53
|
+
body: Buffer;
|
|
54
|
+
expiresAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function defineUrls<const T extends readonly TargetUrlConfig[]>(
|
|
58
|
+
urls: T,
|
|
59
|
+
): T {
|
|
60
|
+
return urls;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class JirenClient<
|
|
64
|
+
T extends readonly TargetUrlConfig[] | Record<string, UrlConfig> =
|
|
65
|
+
| readonly TargetUrlConfig[]
|
|
66
|
+
| Record<string, UrlConfig>,
|
|
67
|
+
> implements Disposable {
|
|
68
|
+
private closed = false;
|
|
69
|
+
private urlMap: Map<string, string> = new Map();
|
|
70
|
+
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
71
|
+
new Map();
|
|
72
|
+
private antibotConfig: Map<string, boolean> = new Map();
|
|
73
|
+
private inflightRequests: Map<string, Promise<any>> = new Map();
|
|
74
|
+
private globalRetry?: RetryConfig;
|
|
75
|
+
private requestInterceptors: RequestInterceptor[] = [];
|
|
76
|
+
private responseInterceptors: ResponseInterceptor[] = [];
|
|
77
|
+
private errorInterceptors: ErrorInterceptor[] = [];
|
|
78
|
+
private targetsPromise: Promise<void> | null = null;
|
|
79
|
+
private targetsComplete: Set<string> = new Set();
|
|
80
|
+
private performanceMode = false;
|
|
81
|
+
private useDefaultHeaders = true;
|
|
82
|
+
private cacheStore: Map<string, CachedResponse> = new Map();
|
|
83
|
+
|
|
84
|
+
private readonly defaultHeaders: Record<string, string> = {
|
|
85
|
+
"user-agent":
|
|
86
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
87
|
+
accept:
|
|
88
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
89
|
+
"accept-encoding": "gzip",
|
|
90
|
+
"accept-language": "en-US,en;q=0.9",
|
|
91
|
+
"sec-ch-ua":
|
|
92
|
+
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
93
|
+
"sec-ch-ua-mobile": "?0",
|
|
94
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
95
|
+
"sec-fetch-dest": "document",
|
|
96
|
+
"sec-fetch-mode": "navigate",
|
|
97
|
+
"sec-fetch-site": "none",
|
|
98
|
+
"sec-fetch-user": "?1",
|
|
99
|
+
"upgrade-insecure-requests": "1",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
public readonly url: UrlAccessor<T>;
|
|
103
|
+
|
|
104
|
+
private metricsCollector: MetricsCollector;
|
|
105
|
+
public readonly metrics: MetricsAPI;
|
|
106
|
+
|
|
107
|
+
constructor(options?: JirenClientOptions<T>) {
|
|
108
|
+
if (!didWarnFallback) {
|
|
109
|
+
didWarnFallback = true;
|
|
110
|
+
console.warn(
|
|
111
|
+
"[Jiren] Using fetch fallback mode (reduced performance/features).",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.metricsCollector = new MetricsCollector();
|
|
116
|
+
this.metrics = this.metricsCollector;
|
|
117
|
+
|
|
118
|
+
this.performanceMode = options?.performanceMode ?? false;
|
|
119
|
+
this.useDefaultHeaders = options?.defaultHeaders ?? true;
|
|
120
|
+
|
|
121
|
+
const targets = options?.urls;
|
|
122
|
+
if (targets) {
|
|
123
|
+
const urls: string[] = [];
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(targets)) {
|
|
126
|
+
for (const item of targets) {
|
|
127
|
+
if (typeof item === "string") {
|
|
128
|
+
urls.push(item);
|
|
129
|
+
} else {
|
|
130
|
+
const config = item as TargetUrlConfig;
|
|
131
|
+
urls.push(config.url);
|
|
132
|
+
this.urlMap.set(config.key, config.url);
|
|
133
|
+
|
|
134
|
+
if (config.cache) {
|
|
135
|
+
const cacheConfig =
|
|
136
|
+
typeof config.cache === "boolean"
|
|
137
|
+
? { enabled: true, ttl: 60000 }
|
|
138
|
+
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
139
|
+
this.cacheConfig.set(config.key, cacheConfig);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (config.antibot) {
|
|
143
|
+
this.antibotConfig.set(config.key, true);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
for (const [key, urlConfig] of Object.entries(targets) as [
|
|
149
|
+
string,
|
|
150
|
+
UrlConfig,
|
|
151
|
+
][]) {
|
|
152
|
+
if (typeof urlConfig === "string") {
|
|
153
|
+
urls.push(urlConfig);
|
|
154
|
+
this.urlMap.set(key, urlConfig);
|
|
155
|
+
} else {
|
|
156
|
+
urls.push(urlConfig.url);
|
|
157
|
+
this.urlMap.set(key, urlConfig.url);
|
|
158
|
+
|
|
159
|
+
if (urlConfig.cache) {
|
|
160
|
+
const cacheConfig =
|
|
161
|
+
typeof urlConfig.cache === "boolean"
|
|
162
|
+
? { enabled: true, ttl: 60000 }
|
|
163
|
+
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
164
|
+
this.cacheConfig.set(key, cacheConfig);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (urlConfig.antibot) {
|
|
168
|
+
this.antibotConfig.set(key, true);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (urls.length > 0 && options?.preconnect !== false) {
|
|
175
|
+
this.targetsPromise = this.preconnect(urls).then(() => {
|
|
176
|
+
urls.forEach((url) => this.targetsComplete.add(url));
|
|
177
|
+
this.targetsPromise = null;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.url = this.createUrlAccessor();
|
|
183
|
+
|
|
184
|
+
if (options?.retry) {
|
|
185
|
+
this.globalRetry =
|
|
186
|
+
typeof options.retry === "number"
|
|
187
|
+
? { count: options.retry, delay: 100, backoff: 2 }
|
|
188
|
+
: options.retry;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (options?.interceptors) {
|
|
192
|
+
this.requestInterceptors = options.interceptors.request || [];
|
|
193
|
+
this.responseInterceptors = options.interceptors.response || [];
|
|
194
|
+
this.errorInterceptors = options.interceptors.error || [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async waitFor(ms: number) {
|
|
199
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public async waitForTargets(): Promise<void> {
|
|
203
|
+
if (this.targetsPromise) await this.targetsPromise;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public async waitForWarmup(): Promise<void> {
|
|
207
|
+
return this.waitForTargets();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private createUrlAccessor(): UrlAccessor<T> {
|
|
211
|
+
const self = this;
|
|
212
|
+
|
|
213
|
+
return new Proxy({} as UrlAccessor<T>, {
|
|
214
|
+
get(_target, prop: string) {
|
|
215
|
+
const baseUrl = self.urlMap.get(prop);
|
|
216
|
+
if (!baseUrl) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`URL key "${prop}" not found. Available keys: ${Array.from(
|
|
219
|
+
self.urlMap.keys(),
|
|
220
|
+
).join(", ")}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const buildUrl = (path?: string) =>
|
|
225
|
+
path
|
|
226
|
+
? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
|
|
227
|
+
: baseUrl;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
get: async <R = any>(
|
|
231
|
+
options?: UrlRequestOptions,
|
|
232
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
233
|
+
if (self.targetsPromise) {
|
|
234
|
+
await self.targetsPromise;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
238
|
+
const useAntibot =
|
|
239
|
+
options?.antibot ?? self.antibotConfig.get(prop) ?? false;
|
|
240
|
+
const url = buildUrl(options?.path);
|
|
241
|
+
const requestKey = self.getRequestKey(
|
|
242
|
+
"GET",
|
|
243
|
+
url,
|
|
244
|
+
options?.headers,
|
|
245
|
+
null,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (cacheConfig?.enabled) {
|
|
249
|
+
const cached = self.getCachedResponse(requestKey);
|
|
250
|
+
if (cached) return cached as JirenResponse<R>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (self.inflightRequests.has(requestKey)) {
|
|
254
|
+
return self.inflightRequests.get(requestKey)!;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const startTime = performance.now();
|
|
258
|
+
const requestPromise = (async () => {
|
|
259
|
+
try {
|
|
260
|
+
const response = await self._request<R>("GET", url, null, {
|
|
261
|
+
headers: options?.headers,
|
|
262
|
+
maxRedirects: options?.maxRedirects,
|
|
263
|
+
responseType: options?.responseType,
|
|
264
|
+
antibot: useAntibot,
|
|
265
|
+
timeout: options?.timeout,
|
|
266
|
+
retry: options?.retry,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (
|
|
270
|
+
cacheConfig?.enabled &&
|
|
271
|
+
self.isJirenResponse(response) &&
|
|
272
|
+
!options?.responseType
|
|
273
|
+
) {
|
|
274
|
+
self.setCachedResponse(requestKey, response, cacheConfig.ttl);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!self.performanceMode && self.isJirenResponse(response)) {
|
|
278
|
+
const raw = response[RAW_BODY];
|
|
279
|
+
self.metricsCollector.recordRequest(prop, {
|
|
280
|
+
startTime,
|
|
281
|
+
responseTimeMs: performance.now() - startTime,
|
|
282
|
+
status: response.status,
|
|
283
|
+
success: response.ok,
|
|
284
|
+
bytesSent: 0,
|
|
285
|
+
bytesReceived: raw.length,
|
|
286
|
+
cacheHit: false,
|
|
287
|
+
dedupeHit: false,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return response;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!self.performanceMode) {
|
|
294
|
+
self.metricsCollector.recordRequest(prop, {
|
|
295
|
+
startTime,
|
|
296
|
+
responseTimeMs: performance.now() - startTime,
|
|
297
|
+
status: 0,
|
|
298
|
+
success: false,
|
|
299
|
+
bytesSent: 0,
|
|
300
|
+
bytesReceived: 0,
|
|
301
|
+
cacheHit: false,
|
|
302
|
+
dedupeHit: false,
|
|
303
|
+
error:
|
|
304
|
+
error instanceof Error ? error.message : String(error),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
} finally {
|
|
309
|
+
self.inflightRequests.delete(requestKey);
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
|
|
313
|
+
self.inflightRequests.set(requestKey, requestPromise);
|
|
314
|
+
return requestPromise;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
post: async <R = any>(
|
|
318
|
+
body?: any,
|
|
319
|
+
options?: UrlRequestOptions & { responseType?: "json" },
|
|
320
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
321
|
+
const { headers: preparedHeaders, serializedBody } =
|
|
322
|
+
self.prepareBody(body, options?.headers);
|
|
323
|
+
return self._request<R>(
|
|
324
|
+
"POST",
|
|
325
|
+
buildUrl(options?.path),
|
|
326
|
+
serializedBody,
|
|
327
|
+
{
|
|
328
|
+
headers: preparedHeaders,
|
|
329
|
+
maxRedirects: options?.maxRedirects,
|
|
330
|
+
responseType: options?.responseType,
|
|
331
|
+
antibot: options?.antibot,
|
|
332
|
+
timeout: options?.timeout,
|
|
333
|
+
retry: options?.retry,
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
put: async <R = any>(
|
|
339
|
+
body?: any,
|
|
340
|
+
options?: UrlRequestOptions & { responseType?: "json" },
|
|
341
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
342
|
+
const { headers: preparedHeaders, serializedBody } =
|
|
343
|
+
self.prepareBody(body, options?.headers);
|
|
344
|
+
return self._request<R>(
|
|
345
|
+
"PUT",
|
|
346
|
+
buildUrl(options?.path),
|
|
347
|
+
serializedBody,
|
|
348
|
+
{
|
|
349
|
+
headers: preparedHeaders,
|
|
350
|
+
maxRedirects: options?.maxRedirects,
|
|
351
|
+
responseType: options?.responseType,
|
|
352
|
+
antibot: options?.antibot,
|
|
353
|
+
timeout: options?.timeout,
|
|
354
|
+
retry: options?.retry,
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
patch: async <R = any>(
|
|
360
|
+
body?: any,
|
|
361
|
+
options?: UrlRequestOptions & { responseType?: "json" },
|
|
362
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
363
|
+
const { headers: preparedHeaders, serializedBody } =
|
|
364
|
+
self.prepareBody(body, options?.headers);
|
|
365
|
+
return self._request<R>(
|
|
366
|
+
"PATCH",
|
|
367
|
+
buildUrl(options?.path),
|
|
368
|
+
serializedBody,
|
|
369
|
+
{
|
|
370
|
+
headers: preparedHeaders,
|
|
371
|
+
maxRedirects: options?.maxRedirects,
|
|
372
|
+
responseType: options?.responseType,
|
|
373
|
+
antibot: options?.antibot,
|
|
374
|
+
timeout: options?.timeout,
|
|
375
|
+
retry: options?.retry,
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
delete: async <R = any>(
|
|
381
|
+
options?: UrlRequestOptions & { responseType?: "json" },
|
|
382
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
383
|
+
const { headers: preparedHeaders, serializedBody } =
|
|
384
|
+
self.prepareBody(options?.body, options?.headers);
|
|
385
|
+
return self._request<R>(
|
|
386
|
+
"DELETE",
|
|
387
|
+
buildUrl(options?.path),
|
|
388
|
+
serializedBody,
|
|
389
|
+
{
|
|
390
|
+
headers: preparedHeaders,
|
|
391
|
+
maxRedirects: options?.maxRedirects,
|
|
392
|
+
responseType: options?.responseType,
|
|
393
|
+
antibot: options?.antibot,
|
|
394
|
+
timeout: options?.timeout,
|
|
395
|
+
retry: options?.retry,
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
head: async (
|
|
401
|
+
options?: UrlRequestOptions,
|
|
402
|
+
): Promise<JirenResponse<any>> => {
|
|
403
|
+
return self._request("HEAD", buildUrl(options?.path), null, {
|
|
404
|
+
headers: options?.headers,
|
|
405
|
+
maxRedirects: options?.maxRedirects,
|
|
406
|
+
antibot: options?.antibot,
|
|
407
|
+
timeout: options?.timeout,
|
|
408
|
+
retry: options?.retry,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
options: async (
|
|
413
|
+
options?: UrlRequestOptions,
|
|
414
|
+
): Promise<JirenResponse<any>> => {
|
|
415
|
+
return self._request("OPTIONS", buildUrl(options?.path), null, {
|
|
416
|
+
headers: options?.headers,
|
|
417
|
+
maxRedirects: options?.maxRedirects,
|
|
418
|
+
antibot: options?.antibot,
|
|
419
|
+
timeout: options?.timeout,
|
|
420
|
+
retry: options?.retry,
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
trace: async <R = any>(
|
|
425
|
+
options?: UrlRequestOptions,
|
|
426
|
+
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
427
|
+
return self._request<R>("TRACE", buildUrl(options?.path), null, {
|
|
428
|
+
headers: options?.headers,
|
|
429
|
+
maxRedirects: options?.maxRedirects,
|
|
430
|
+
antibot: options?.antibot,
|
|
431
|
+
timeout: options?.timeout,
|
|
432
|
+
retry: options?.retry,
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
prefetch: async (options?: UrlRequestOptions): Promise<void> => {
|
|
437
|
+
const targetUrl = buildUrl(options?.path);
|
|
438
|
+
for (const key of self.cacheStore.keys()) {
|
|
439
|
+
if (key.includes(targetUrl)) {
|
|
440
|
+
self.cacheStore.delete(key);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
await self._request("GET", targetUrl, null, {
|
|
444
|
+
headers: options?.headers,
|
|
445
|
+
maxRedirects: options?.maxRedirects,
|
|
446
|
+
antibot: options?.antibot,
|
|
447
|
+
timeout: options?.timeout,
|
|
448
|
+
retry: options?.retry,
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
download: async <R = any>(
|
|
453
|
+
options?: ProgressRequestOptions,
|
|
454
|
+
): Promise<JirenResponse<R>> => {
|
|
455
|
+
const response = (await self._request<R>(
|
|
456
|
+
"GET",
|
|
457
|
+
buildUrl(options?.path),
|
|
458
|
+
null,
|
|
459
|
+
{ headers: options?.headers },
|
|
460
|
+
)) as InternalResponse<R>;
|
|
461
|
+
|
|
462
|
+
const total = response[RAW_BODY].length;
|
|
463
|
+
options?.onDownloadProgress?.({
|
|
464
|
+
loaded: total,
|
|
465
|
+
total,
|
|
466
|
+
percent: 100,
|
|
467
|
+
speed: 0,
|
|
468
|
+
eta: 0,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return self.cloneResponse(response);
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
upload: async <R = any>(
|
|
475
|
+
options?: ProgressRequestOptions & {
|
|
476
|
+
method?: "POST" | "PUT" | "PATCH";
|
|
477
|
+
body?: string | object | null;
|
|
478
|
+
},
|
|
479
|
+
): Promise<JirenResponse<R>> => {
|
|
480
|
+
const method = options?.method || "POST";
|
|
481
|
+
const { headers: preparedHeaders, serializedBody } = self.prepareBody(
|
|
482
|
+
options?.body,
|
|
483
|
+
options?.headers,
|
|
484
|
+
);
|
|
485
|
+
const total = serializedBody
|
|
486
|
+
? Buffer.byteLength(serializedBody, "utf-8")
|
|
487
|
+
: 0;
|
|
488
|
+
|
|
489
|
+
if (options?.onUploadProgress) {
|
|
490
|
+
options.onUploadProgress({
|
|
491
|
+
loaded: 0,
|
|
492
|
+
total,
|
|
493
|
+
percent: 0,
|
|
494
|
+
speed: 0,
|
|
495
|
+
eta: 0,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const response = (await self._request<R>(
|
|
500
|
+
method,
|
|
501
|
+
buildUrl(options?.path),
|
|
502
|
+
serializedBody,
|
|
503
|
+
{ headers: preparedHeaders },
|
|
504
|
+
)) as InternalResponse<R>;
|
|
505
|
+
|
|
506
|
+
if (options?.onUploadProgress) {
|
|
507
|
+
options.onUploadProgress({
|
|
508
|
+
loaded: total,
|
|
509
|
+
total,
|
|
510
|
+
percent: 100,
|
|
511
|
+
speed: 0,
|
|
512
|
+
eta: 0,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return self.cloneResponse(response);
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
getJsonFields: async <R = any>(
|
|
520
|
+
fields: string[],
|
|
521
|
+
options?: UrlRequestOptions,
|
|
522
|
+
): Promise<R> => {
|
|
523
|
+
const json = await self._request<Record<string, unknown>>(
|
|
524
|
+
"GET",
|
|
525
|
+
buildUrl(options?.path),
|
|
526
|
+
null,
|
|
527
|
+
{
|
|
528
|
+
headers: options?.headers,
|
|
529
|
+
maxRedirects: options?.maxRedirects,
|
|
530
|
+
timeout: options?.timeout,
|
|
531
|
+
responseType: "json",
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const result: Record<string, unknown> = {};
|
|
536
|
+
for (const field of fields) {
|
|
537
|
+
result[field] = json?.[field];
|
|
538
|
+
}
|
|
539
|
+
return result as R;
|
|
540
|
+
},
|
|
541
|
+
} as UrlEndpoint;
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
public close(): void {
|
|
547
|
+
this.closed = true;
|
|
548
|
+
this.inflightRequests.clear();
|
|
549
|
+
this.cacheStore.clear();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
[Symbol.dispose](): void {
|
|
553
|
+
this.close();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
public use(interceptors: Interceptors): this {
|
|
557
|
+
if (interceptors.request)
|
|
558
|
+
this.requestInterceptors.push(...interceptors.request);
|
|
559
|
+
if (interceptors.response)
|
|
560
|
+
this.responseInterceptors.push(...interceptors.response);
|
|
561
|
+
if (interceptors.error) this.errorInterceptors.push(...interceptors.error);
|
|
562
|
+
return this;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
public async preconnect(urls: string[]): Promise<void> {
|
|
566
|
+
urls.forEach((url) => this.targetsComplete.add(url));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
public async warmup(urls: string[]): Promise<void> {
|
|
570
|
+
return this.preconnect(urls);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
public prefetch(urls: string[]): void {
|
|
574
|
+
void this.preconnect(urls);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
protected async _request<T = any>(
|
|
578
|
+
method: string,
|
|
579
|
+
url: string,
|
|
580
|
+
body?: string | null,
|
|
581
|
+
options?: RequestOptions & { responseType: "json" },
|
|
582
|
+
): Promise<T>;
|
|
583
|
+
protected async _request<T = any>(
|
|
584
|
+
method: string,
|
|
585
|
+
url: string,
|
|
586
|
+
body?: string | null,
|
|
587
|
+
options?: RequestOptions & { responseType: "text" },
|
|
588
|
+
): Promise<string>;
|
|
589
|
+
protected async _request<T = any>(
|
|
590
|
+
method: string,
|
|
591
|
+
url: string,
|
|
592
|
+
body?: string | null,
|
|
593
|
+
options?: RequestOptions & { responseType: "arraybuffer" },
|
|
594
|
+
): Promise<ArrayBuffer>;
|
|
595
|
+
protected async _request<T = any>(
|
|
596
|
+
method: string,
|
|
597
|
+
url: string,
|
|
598
|
+
body?: string | null,
|
|
599
|
+
options?: RequestOptions & { responseType: "blob" },
|
|
600
|
+
): Promise<Blob>;
|
|
601
|
+
protected async _request<T = any>(
|
|
602
|
+
method: string,
|
|
603
|
+
url: string,
|
|
604
|
+
body?: string | null,
|
|
605
|
+
options?: RequestOptions,
|
|
606
|
+
): Promise<JirenResponse<T>>;
|
|
607
|
+
protected async _request<T = any>(
|
|
608
|
+
method: string,
|
|
609
|
+
url: string,
|
|
610
|
+
body?: string | null,
|
|
611
|
+
options?: RequestOptions | Record<string, string> | null,
|
|
612
|
+
): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
|
|
613
|
+
if (this.closed) throw new Error("Client is closed");
|
|
614
|
+
|
|
615
|
+
let headers: Record<string, string> = {};
|
|
616
|
+
let responseType: RequestOptions["responseType"] | undefined;
|
|
617
|
+
let timeout: number | undefined;
|
|
618
|
+
|
|
619
|
+
if (options) {
|
|
620
|
+
if (
|
|
621
|
+
"maxRedirects" in options ||
|
|
622
|
+
"headers" in options ||
|
|
623
|
+
"responseType" in options ||
|
|
624
|
+
"method" in options ||
|
|
625
|
+
"timeout" in options ||
|
|
626
|
+
"antibot" in options
|
|
627
|
+
) {
|
|
628
|
+
const opts = options as RequestOptions;
|
|
629
|
+
if (opts.headers) headers = opts.headers;
|
|
630
|
+
if (opts.responseType) responseType = opts.responseType;
|
|
631
|
+
if (opts.timeout !== undefined) timeout = opts.timeout;
|
|
632
|
+
} else {
|
|
633
|
+
headers = options as Record<string, string>;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let ctx: InterceptorRequestContext = { method, url, headers, body };
|
|
638
|
+
if (this.requestInterceptors.length > 0) {
|
|
639
|
+
for (const interceptor of this.requestInterceptors) {
|
|
640
|
+
ctx = await interceptor(ctx);
|
|
641
|
+
}
|
|
642
|
+
method = ctx.method;
|
|
643
|
+
url = ctx.url;
|
|
644
|
+
headers = ctx.headers;
|
|
645
|
+
body = ctx.body ?? null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let retryConfig = this.globalRetry;
|
|
649
|
+
if (options && typeof options === "object" && "retry" in options) {
|
|
650
|
+
const userRetry = options?.retry;
|
|
651
|
+
if (typeof userRetry === "number") {
|
|
652
|
+
retryConfig = { count: userRetry, delay: 100, backoff: 2 };
|
|
653
|
+
} else if (typeof userRetry === "object") {
|
|
654
|
+
retryConfig = userRetry;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const finalHeaders = this.buildHeaders(headers);
|
|
659
|
+
let attempts = 0;
|
|
660
|
+
const maxAttempts = (retryConfig?.count || 0) + 1;
|
|
661
|
+
let currentDelay = retryConfig?.delay || 100;
|
|
662
|
+
const backoff = retryConfig?.backoff || 2;
|
|
663
|
+
let lastError: unknown;
|
|
664
|
+
|
|
665
|
+
while (attempts < maxAttempts) {
|
|
666
|
+
attempts++;
|
|
667
|
+
try {
|
|
668
|
+
const controller =
|
|
669
|
+
timeout && timeout > 0 ? new AbortController() : undefined;
|
|
670
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
671
|
+
|
|
672
|
+
if (controller && timeout && timeout > 0) {
|
|
673
|
+
timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const response = await fetch(url, {
|
|
678
|
+
method,
|
|
679
|
+
headers: finalHeaders,
|
|
680
|
+
body: body || undefined,
|
|
681
|
+
redirect: "follow",
|
|
682
|
+
signal: controller?.signal,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const raw = Buffer.from(await response.arrayBuffer());
|
|
686
|
+
const parsed = this.createResponse<T>(url, response, raw);
|
|
687
|
+
|
|
688
|
+
let finalResponse = parsed;
|
|
689
|
+
if (this.responseInterceptors.length > 0) {
|
|
690
|
+
let responseCtx: InterceptorResponseContext<T> = {
|
|
691
|
+
request: ctx,
|
|
692
|
+
response: parsed,
|
|
693
|
+
};
|
|
694
|
+
for (const interceptor of this.responseInterceptors) {
|
|
695
|
+
responseCtx = await interceptor(responseCtx);
|
|
696
|
+
}
|
|
697
|
+
finalResponse = responseCtx.response as InternalResponse<T>;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (responseType === "json") return finalResponse.body.json();
|
|
701
|
+
if (responseType === "text") return finalResponse.body.text();
|
|
702
|
+
if (responseType === "arraybuffer")
|
|
703
|
+
return finalResponse.body.arrayBuffer();
|
|
704
|
+
if (responseType === "blob") return finalResponse.body.blob();
|
|
705
|
+
|
|
706
|
+
return finalResponse;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if ((error as Error).name === "AbortError" && timeout) {
|
|
709
|
+
const timeoutError = new Error(`Request timeout after ${timeout}ms`);
|
|
710
|
+
timeoutError.name = "TimeoutError";
|
|
711
|
+
throw timeoutError;
|
|
712
|
+
}
|
|
713
|
+
throw error;
|
|
714
|
+
} finally {
|
|
715
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
716
|
+
}
|
|
717
|
+
} catch (err) {
|
|
718
|
+
if (this.errorInterceptors.length > 0) {
|
|
719
|
+
for (const interceptor of this.errorInterceptors) {
|
|
720
|
+
await interceptor(err as Error, ctx);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
lastError = err;
|
|
724
|
+
|
|
725
|
+
if ((err as Error).name === "TimeoutError") {
|
|
726
|
+
throw err;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (attempts < maxAttempts) {
|
|
730
|
+
await this.waitFor(currentDelay);
|
|
731
|
+
currentDelay *= backoff;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
throw lastError || new Error("Request failed after retries");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private createResponse<T>(
|
|
740
|
+
url: string,
|
|
741
|
+
response: Response,
|
|
742
|
+
buffer: Buffer,
|
|
743
|
+
): InternalResponse<T> {
|
|
744
|
+
const headers: Record<string, string> = {};
|
|
745
|
+
response.headers.forEach((value, key) => {
|
|
746
|
+
headers[key.toLowerCase()] = value;
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
let bodyUsed = false;
|
|
750
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
751
|
+
bodyUsed: false,
|
|
752
|
+
arrayBuffer: async () => {
|
|
753
|
+
bodyUsed = true;
|
|
754
|
+
return buffer.buffer.slice(
|
|
755
|
+
buffer.byteOffset,
|
|
756
|
+
buffer.byteOffset + buffer.byteLength,
|
|
757
|
+
) as ArrayBuffer;
|
|
758
|
+
},
|
|
759
|
+
blob: async () => {
|
|
760
|
+
bodyUsed = true;
|
|
761
|
+
return new Blob([buffer]);
|
|
762
|
+
},
|
|
763
|
+
text: async () => {
|
|
764
|
+
bodyUsed = true;
|
|
765
|
+
return buffer.toString("utf-8");
|
|
766
|
+
},
|
|
767
|
+
json: async <R = T>(): Promise<R> => {
|
|
768
|
+
bodyUsed = true;
|
|
769
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
774
|
+
get: () => bodyUsed,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const jirenResponse: InternalResponse<T> = {
|
|
778
|
+
url,
|
|
779
|
+
status: response.status,
|
|
780
|
+
statusText: response.statusText || STATUS_TEXT[response.status] || "",
|
|
781
|
+
headers,
|
|
782
|
+
ok: response.ok,
|
|
783
|
+
redirected: response.redirected,
|
|
784
|
+
type: response.type || "basic",
|
|
785
|
+
body: bodyObj,
|
|
786
|
+
[RAW_BODY]: buffer,
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
return jirenResponse;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private cloneResponse<T>(response: InternalResponse<T>): JirenResponse<T> {
|
|
793
|
+
const clone = this.createBufferedResponse<T>(
|
|
794
|
+
response.url,
|
|
795
|
+
response.status,
|
|
796
|
+
response.statusText,
|
|
797
|
+
response.headers,
|
|
798
|
+
response.ok,
|
|
799
|
+
response.redirected,
|
|
800
|
+
response.type,
|
|
801
|
+
response[RAW_BODY],
|
|
802
|
+
);
|
|
803
|
+
return clone;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private createBufferedResponse<T>(
|
|
807
|
+
url: string,
|
|
808
|
+
status: number,
|
|
809
|
+
statusText: string,
|
|
810
|
+
headers: Record<string, string>,
|
|
811
|
+
ok: boolean,
|
|
812
|
+
redirected: boolean,
|
|
813
|
+
type: "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect",
|
|
814
|
+
buffer: Buffer,
|
|
815
|
+
): InternalResponse<T> {
|
|
816
|
+
let bodyUsed = false;
|
|
817
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
818
|
+
bodyUsed: false,
|
|
819
|
+
arrayBuffer: async () => {
|
|
820
|
+
bodyUsed = true;
|
|
821
|
+
return buffer.buffer.slice(
|
|
822
|
+
buffer.byteOffset,
|
|
823
|
+
buffer.byteOffset + buffer.byteLength,
|
|
824
|
+
) as ArrayBuffer;
|
|
825
|
+
},
|
|
826
|
+
blob: async () => {
|
|
827
|
+
bodyUsed = true;
|
|
828
|
+
return new Blob([buffer]);
|
|
829
|
+
},
|
|
830
|
+
text: async () => {
|
|
831
|
+
bodyUsed = true;
|
|
832
|
+
return buffer.toString("utf-8");
|
|
833
|
+
},
|
|
834
|
+
json: async <R = T>(): Promise<R> => {
|
|
835
|
+
bodyUsed = true;
|
|
836
|
+
return JSON.parse(buffer.toString("utf-8"));
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
841
|
+
get: () => bodyUsed,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
return {
|
|
845
|
+
url,
|
|
846
|
+
status,
|
|
847
|
+
statusText,
|
|
848
|
+
headers: { ...headers },
|
|
849
|
+
ok,
|
|
850
|
+
redirected,
|
|
851
|
+
type,
|
|
852
|
+
body: bodyObj,
|
|
853
|
+
[RAW_BODY]: Buffer.from(buffer),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private isJirenResponse(value: unknown): value is InternalResponse<any> {
|
|
858
|
+
return (
|
|
859
|
+
typeof value === "object" &&
|
|
860
|
+
value !== null &&
|
|
861
|
+
"status" in value &&
|
|
862
|
+
"body" in value
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private prepareBody(
|
|
867
|
+
body: string | object | null | undefined,
|
|
868
|
+
userHeaders?: Record<string, string>,
|
|
869
|
+
): { headers: Record<string, string>; serializedBody: string | null } {
|
|
870
|
+
let serializedBody: string | null = null;
|
|
871
|
+
const headers = { ...userHeaders };
|
|
872
|
+
|
|
873
|
+
if (body !== null && body !== undefined) {
|
|
874
|
+
if (typeof body === "object") {
|
|
875
|
+
serializedBody = JSON.stringify(body);
|
|
876
|
+
const hasContentType = Object.keys(headers).some(
|
|
877
|
+
(k) => k.toLowerCase() === "content-type",
|
|
878
|
+
);
|
|
879
|
+
if (!hasContentType) {
|
|
880
|
+
headers["Content-Type"] = "application/json";
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
serializedBody = String(body);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return { headers, serializedBody };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private buildHeaders(
|
|
891
|
+
userHeaders: Record<string, string>,
|
|
892
|
+
): Record<string, string> {
|
|
893
|
+
if (!this.useDefaultHeaders) {
|
|
894
|
+
return { ...userHeaders };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const normalized: Record<string, string> = {};
|
|
898
|
+
for (const [key, value] of Object.entries(this.defaultHeaders)) {
|
|
899
|
+
normalized[key.toLowerCase()] = value;
|
|
900
|
+
}
|
|
901
|
+
for (const [key, value] of Object.entries(userHeaders)) {
|
|
902
|
+
normalized[key.toLowerCase()] = value;
|
|
903
|
+
}
|
|
904
|
+
return normalized;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private getRequestKey(
|
|
908
|
+
method: string,
|
|
909
|
+
url: string,
|
|
910
|
+
headers?: Record<string, string>,
|
|
911
|
+
body?: string | null,
|
|
912
|
+
): string {
|
|
913
|
+
return `${method}:${url}:${JSON.stringify(headers || {})}:${body || ""}`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private getCachedResponse(key: string): JirenResponse | null {
|
|
917
|
+
const cached = this.cacheStore.get(key);
|
|
918
|
+
if (!cached) return null;
|
|
919
|
+
if (Date.now() > cached.expiresAt) {
|
|
920
|
+
this.cacheStore.delete(key);
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return this.createBufferedResponse(
|
|
925
|
+
cached.url,
|
|
926
|
+
cached.status,
|
|
927
|
+
cached.statusText,
|
|
928
|
+
cached.headers,
|
|
929
|
+
cached.ok,
|
|
930
|
+
cached.redirected,
|
|
931
|
+
cached.type,
|
|
932
|
+
cached.body,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private setCachedResponse(
|
|
937
|
+
key: string,
|
|
938
|
+
response: InternalResponse,
|
|
939
|
+
ttl: number,
|
|
940
|
+
): void {
|
|
941
|
+
this.cacheStore.set(key, {
|
|
942
|
+
status: response.status,
|
|
943
|
+
statusText: response.statusText,
|
|
944
|
+
headers: { ...response.headers },
|
|
945
|
+
url: response.url,
|
|
946
|
+
ok: response.ok,
|
|
947
|
+
redirected: response.redirected,
|
|
948
|
+
type: response.type,
|
|
949
|
+
body: Buffer.from(response[RAW_BODY]),
|
|
950
|
+
expiresAt: Date.now() + ttl,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|