jiren 1.4.5 → 1.5.5

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.
@@ -1,473 +0,0 @@
1
- import { ResponseCache } from "./cache";
2
- import type {
3
- RequestOptions,
4
- JirenResponse,
5
- JirenResponseBody,
6
- WarmupUrlConfig,
7
- UrlRequestOptions,
8
- UrlEndpoint,
9
- CacheConfig,
10
- } from "./types";
11
-
12
- /** URL configuration with optional cache */
13
- export type UrlConfig = string | { url: string; cache?: boolean | CacheConfig };
14
-
15
- /** Options for JirenClient constructor */
16
- export interface JirenClientOptions<
17
- T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
18
- | readonly WarmupUrlConfig[]
19
- | Record<string, UrlConfig>
20
- > {
21
- /** URLs to warmup (pre-connect) */
22
- warmup?: string[] | T;
23
-
24
- /** Benchmark mode (Ignored in Node.js fallback) */
25
- benchmark?: boolean;
26
- }
27
-
28
- /** Helper to extract keys from Warmup Config */
29
- export type ExtractWarmupKeys<
30
- T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
31
- > = T extends readonly WarmupUrlConfig[]
32
- ? T[number]["key"]
33
- : T extends Record<string, UrlConfig>
34
- ? keyof T
35
- : never;
36
-
37
- /** Type-safe URL accessor - maps keys to UrlEndpoint objects */
38
- export type UrlAccessor<
39
- T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
40
- > = {
41
- [K in ExtractWarmupKeys<T>]: UrlEndpoint;
42
- };
43
-
44
- /**
45
- * Helper function to define warmup URLs with type inference.
46
- */
47
- export function defineUrls<const T extends readonly WarmupUrlConfig[]>(
48
- urls: T
49
- ): T {
50
- return urls;
51
- }
52
-
53
- export class JirenClient<
54
- T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
55
- | readonly WarmupUrlConfig[]
56
- | Record<string, UrlConfig>
57
- > {
58
- private urlMap: Map<string, string> = new Map();
59
- private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
60
- new Map();
61
- private antibotConfig: Map<string, boolean> = new Map();
62
- private cache: ResponseCache;
63
-
64
- /** Type-safe URL accessor for warmed-up URLs */
65
- public readonly url: UrlAccessor<T>;
66
-
67
- constructor(options?: JirenClientOptions<T>) {
68
- // Initialize cache
69
- this.cache = new ResponseCache(100);
70
-
71
- // Process warmup URLs
72
- if (options?.warmup) {
73
- const urls: string[] = [];
74
- const warmup = options.warmup;
75
-
76
- if (Array.isArray(warmup)) {
77
- for (const item of warmup) {
78
- if (typeof item === "string") {
79
- urls.push(item);
80
- } else {
81
- // WarmupUrlConfig with key and optional cache
82
- const config = item as WarmupUrlConfig;
83
- urls.push(config.url);
84
- this.urlMap.set(config.key, config.url);
85
-
86
- // Store cache config
87
- if (config.cache) {
88
- const cacheConfig =
89
- typeof config.cache === "boolean"
90
- ? { enabled: true, ttl: 60000 }
91
- : { enabled: true, ttl: config.cache.ttl || 60000 };
92
- this.cacheConfig.set(config.key, cacheConfig);
93
- }
94
-
95
- // Store antibot config
96
- if (config.antibot) {
97
- this.antibotConfig.set(config.key, true);
98
- }
99
- }
100
- }
101
- } else {
102
- // Record<string, UrlConfig>
103
- for (const [key, urlConfig] of Object.entries(warmup)) {
104
- if (typeof urlConfig === "string") {
105
- // Simple string URL
106
- urls.push(urlConfig);
107
- this.urlMap.set(key, urlConfig);
108
- } else {
109
- // URL config object with cache
110
- urls.push(urlConfig.url);
111
- this.urlMap.set(key, urlConfig.url);
112
-
113
- // Store cache config
114
- if (urlConfig.cache) {
115
- const cacheConfig =
116
- typeof urlConfig.cache === "boolean"
117
- ? { enabled: true, ttl: 60000 }
118
- : { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
119
- this.cacheConfig.set(key, cacheConfig);
120
- }
121
-
122
- // Store antibot config
123
- if ((urlConfig as { antibot?: boolean }).antibot) {
124
- this.antibotConfig.set(key, true);
125
- }
126
- }
127
- }
128
- }
129
-
130
- if (urls.length > 0) {
131
- this.warmup(urls);
132
- }
133
-
134
- // Preload L2 disk cache entries into L1 memory for cached endpoints
135
- // This ensures user's first request hits L1 (~0.001ms) instead of L2 (~5ms)
136
- for (const [key, config] of this.cacheConfig.entries()) {
137
- if (config.enabled) {
138
- const url = this.urlMap.get(key);
139
- if (url) {
140
- this.cache.preloadL1(url);
141
- }
142
- }
143
- }
144
- }
145
-
146
- // Create proxy for type-safe URL access
147
- this.url = this.createUrlAccessor();
148
- }
149
-
150
- /**
151
- * Wait for warmup to complete (no-op for fetch client)
152
- */
153
- public async waitForWarmup(): Promise<void> {
154
- // No-op - fetch client warmup is fire-and-forget
155
- }
156
-
157
- /**
158
- * Creates a proxy-based URL accessor for type-safe access.
159
- */
160
- private createUrlAccessor(): UrlAccessor<T> {
161
- const self = this;
162
-
163
- return new Proxy({} as UrlAccessor<T>, {
164
- get(_target, prop: string) {
165
- const baseUrl = self.urlMap.get(prop);
166
- if (!baseUrl) {
167
- throw new Error(
168
- `URL key "${prop}" not found. Available keys: ${Array.from(
169
- self.urlMap.keys()
170
- ).join(", ")}`
171
- );
172
- }
173
-
174
- const buildUrl = (path?: string) =>
175
- path
176
- ? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
177
- : baseUrl;
178
-
179
- return {
180
- get: async <R = any>(
181
- options?: UrlRequestOptions
182
- ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
183
- const cacheConfig = self.cacheConfig.get(prop);
184
-
185
- // Check if antibot is enabled for this URL (from warmup config or per-request)
186
- const useAntibot =
187
- options?.antibot ?? self.antibotConfig.get(prop) ?? false;
188
-
189
- if (cacheConfig?.enabled) {
190
- const cached = self.cache.get(baseUrl, options?.path, options);
191
- if (cached) return cached as any;
192
- }
193
-
194
- const response = await self.request<R>(
195
- "GET",
196
- buildUrl(options?.path),
197
- null,
198
- {
199
- headers: options?.headers,
200
- maxRedirects: options?.maxRedirects,
201
- responseType: options?.responseType,
202
- antibot: useAntibot,
203
- }
204
- );
205
-
206
- if (
207
- cacheConfig?.enabled &&
208
- typeof response === "object" &&
209
- "status" in response
210
- ) {
211
- self.cache.set(
212
- baseUrl,
213
- response as JirenResponse,
214
- cacheConfig.ttl,
215
- options?.path,
216
- options
217
- );
218
- }
219
-
220
- return response;
221
- },
222
-
223
- post: async <R = any>(
224
- body?: string | null,
225
- options?: UrlRequestOptions
226
- ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
227
- return self.request<R>(
228
- "POST",
229
- buildUrl(options?.path),
230
- body || null,
231
- {
232
- headers: options?.headers,
233
- maxRedirects: options?.maxRedirects,
234
- responseType: options?.responseType,
235
- }
236
- );
237
- },
238
-
239
- put: async <R = any>(
240
- body?: string | null,
241
- options?: UrlRequestOptions
242
- ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
243
- return self.request<R>(
244
- "PUT",
245
- buildUrl(options?.path),
246
- body || null,
247
- {
248
- headers: options?.headers,
249
- maxRedirects: options?.maxRedirects,
250
- responseType: options?.responseType,
251
- }
252
- );
253
- },
254
-
255
- patch: async <R = any>(
256
- body?: string | null,
257
- options?: UrlRequestOptions
258
- ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
259
- return self.request<R>(
260
- "PATCH",
261
- buildUrl(options?.path),
262
- body || null,
263
- {
264
- headers: options?.headers,
265
- maxRedirects: options?.maxRedirects,
266
- responseType: options?.responseType,
267
- }
268
- );
269
- },
270
-
271
- delete: async <R = any>(
272
- body?: string | null,
273
- options?: UrlRequestOptions
274
- ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
275
- return self.request<R>(
276
- "DELETE",
277
- buildUrl(options?.path),
278
- body || null,
279
- {
280
- headers: options?.headers,
281
- maxRedirects: options?.maxRedirects,
282
- responseType: options?.responseType,
283
- }
284
- );
285
- },
286
-
287
- head: async (
288
- options?: UrlRequestOptions
289
- ): Promise<JirenResponse<any>> => {
290
- return self.request("HEAD", buildUrl(options?.path), null, {
291
- headers: options?.headers,
292
- maxRedirects: options?.maxRedirects,
293
- antibot: options?.antibot,
294
- });
295
- },
296
-
297
- options: async (
298
- options?: UrlRequestOptions
299
- ): Promise<JirenResponse<any>> => {
300
- return self.request("OPTIONS", buildUrl(options?.path), null, {
301
- headers: options?.headers,
302
- maxRedirects: options?.maxRedirects,
303
- antibot: options?.antibot,
304
- });
305
- },
306
-
307
- prefetch: async (options?: UrlRequestOptions): Promise<void> => {
308
- self.cache.clear(baseUrl);
309
- const cacheConfig = self.cacheConfig.get(prop);
310
- if (cacheConfig?.enabled) {
311
- await self.request("GET", buildUrl(options?.path), null, {
312
- headers: options?.headers,
313
- maxRedirects: options?.maxRedirects,
314
- antibot: options?.antibot,
315
- });
316
- }
317
- },
318
- } as UrlEndpoint;
319
- },
320
- });
321
- }
322
-
323
- public close(): void {
324
- // No-op for fetch client
325
- }
326
-
327
- public async warmup(urls: string[]): Promise<void> {
328
- // Basic prefetch for Node.js fallback (HEAD request)
329
- await Promise.allSettled(
330
- urls.map((url) =>
331
- fetch(url, { method: "HEAD", signal: AbortSignal.timeout(2000) }).catch(
332
- () => {}
333
- )
334
- )
335
- );
336
- }
337
-
338
- /**
339
- * @deprecated Use warmup() instead
340
- */
341
- public prefetch(urls: string[]): void {
342
- this.warmup(urls);
343
- }
344
-
345
- public async request<T = any>(
346
- method: string,
347
- url: string,
348
- body?: string | null,
349
- options?: RequestOptions & { responseType: "json" }
350
- ): Promise<T>;
351
- public async request<T = any>(
352
- method: string,
353
- url: string,
354
- body?: string | null,
355
- options?: RequestOptions & { responseType: "text" }
356
- ): Promise<string>;
357
- public async request<T = any>(
358
- method: string,
359
- url: string,
360
- body?: string | null,
361
- options?: RequestOptions & { responseType: "arraybuffer" }
362
- ): Promise<ArrayBuffer>;
363
- public async request<T = any>(
364
- method: string,
365
- url: string,
366
- body?: string | null,
367
- options?: RequestOptions & { responseType: "blob" }
368
- ): Promise<Blob>;
369
- public async request<T = any>(
370
- method: string,
371
- url: string,
372
- body?: string | null,
373
- options?: RequestOptions
374
- ): Promise<JirenResponse<T>>;
375
- public async request<T = any>(
376
- method: string,
377
- url: string,
378
- body?: string | null,
379
- options?: RequestOptions | Record<string, string> | null
380
- ): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
381
- // Normalize options
382
- let headers: Record<string, string> = {};
383
- let responseType: RequestOptions["responseType"] | undefined;
384
-
385
- if (options) {
386
- if (
387
- "maxRedirects" in options ||
388
- "headers" in options ||
389
- "responseType" in options ||
390
- "method" in options ||
391
- "timeout" in options ||
392
- "antibot" in options
393
- ) {
394
- const opts = options as RequestOptions;
395
- if (opts.headers) headers = opts.headers;
396
- if (opts.responseType) responseType = opts.responseType;
397
- } else {
398
- headers = options as Record<string, string>;
399
- }
400
- }
401
-
402
- const fetchOptions: RequestInit = {
403
- method,
404
- headers: headers,
405
- body: body || undefined,
406
- redirect: "follow", // Native client follows redirects
407
- };
408
-
409
- const response = await fetch(url, fetchOptions);
410
-
411
- // Auto-parse if requested
412
- if (responseType) {
413
- if (responseType === "json") return response.json() as Promise<T>;
414
- if (responseType === "text") return response.text();
415
- if (responseType === "arraybuffer") return response.arrayBuffer();
416
- if (responseType === "blob") return response.blob();
417
- }
418
-
419
- // Construct JirenResponse
420
- const responseHeaders: Record<string, string> = {};
421
- response.headers.forEach((val, key) => {
422
- responseHeaders[key] = val;
423
- });
424
-
425
- // Create a clone for each reading method to avoid "body used" error if user reads multiple times
426
- // However, fetch body can only be read once. We replicate the behavior of the native client
427
- // where we can read it. To do that we might need to buffer it OR just expose the raw response.
428
- // The native client implementation buffers the whole body. Let's match that.
429
-
430
- const buffer = await response.arrayBuffer();
431
-
432
- let bodyUsed = false;
433
- const consumeBody = () => {
434
- bodyUsed = true;
435
- };
436
-
437
- const bodyObj: JirenResponseBody<T> = {
438
- bodyUsed: false,
439
- arrayBuffer: async () => {
440
- consumeBody();
441
- return buffer.slice(0);
442
- },
443
- blob: async () => {
444
- consumeBody();
445
- return new Blob([buffer]);
446
- },
447
- text: async () => {
448
- consumeBody();
449
- return new TextDecoder().decode(buffer);
450
- },
451
- json: async <R = T>(): Promise<R> => {
452
- consumeBody();
453
- const text = new TextDecoder().decode(buffer);
454
- return JSON.parse(text);
455
- },
456
- };
457
-
458
- Object.defineProperty(bodyObj, "bodyUsed", {
459
- get: () => bodyUsed,
460
- });
461
-
462
- return {
463
- url: response.url,
464
- status: response.status,
465
- statusText: response.statusText,
466
- headers: responseHeaders,
467
- ok: response.ok,
468
- redirected: response.redirected,
469
- type: response.type,
470
- body: bodyObj,
471
- } as JirenResponse<T>;
472
- }
473
- }