jiren 1.4.0 → 1.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 +220 -3
- package/components/cache.ts +398 -41
- package/components/client-node-native.ts +45 -12
- package/components/client.ts +524 -99
- package/components/index.ts +9 -0
- package/components/metrics.ts +420 -0
- package/components/native-cache.ts +181 -0
- package/components/native-node.ts +26 -0
- package/components/native.ts +92 -0
- package/components/types.ts +105 -5
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +1 -1
- package/components/client-node.ts +0 -440
package/components/native.ts
CHANGED
|
@@ -81,6 +81,98 @@ export const ffiDef = {
|
|
|
81
81
|
args: [FFIType.ptr, FFIType.bool],
|
|
82
82
|
returns: FFIType.void,
|
|
83
83
|
},
|
|
84
|
+
|
|
85
|
+
// =========================================================================
|
|
86
|
+
// OPTIMIZED SINGLE-CALL API
|
|
87
|
+
// =========================================================================
|
|
88
|
+
|
|
89
|
+
zclient_request_full: {
|
|
90
|
+
args: [
|
|
91
|
+
FFIType.ptr, // client
|
|
92
|
+
FFIType.cstring, // method
|
|
93
|
+
FFIType.cstring, // url
|
|
94
|
+
FFIType.cstring, // headers (nullable)
|
|
95
|
+
FFIType.cstring, // body (nullable)
|
|
96
|
+
FFIType.u8, // max_redirects
|
|
97
|
+
FFIType.bool, // antibot
|
|
98
|
+
],
|
|
99
|
+
returns: FFIType.ptr, // ZFullResponse*
|
|
100
|
+
},
|
|
101
|
+
zclient_response_full_free: {
|
|
102
|
+
args: [FFIType.ptr],
|
|
103
|
+
returns: FFIType.void,
|
|
104
|
+
},
|
|
105
|
+
// Accessor functions for ZFullResponse
|
|
106
|
+
zfull_response_status: {
|
|
107
|
+
args: [FFIType.ptr],
|
|
108
|
+
returns: FFIType.u16,
|
|
109
|
+
},
|
|
110
|
+
zfull_response_body: {
|
|
111
|
+
args: [FFIType.ptr],
|
|
112
|
+
returns: FFIType.ptr,
|
|
113
|
+
},
|
|
114
|
+
zfull_response_body_len: {
|
|
115
|
+
args: [FFIType.ptr],
|
|
116
|
+
returns: FFIType.u64,
|
|
117
|
+
},
|
|
118
|
+
zfull_response_headers: {
|
|
119
|
+
args: [FFIType.ptr],
|
|
120
|
+
returns: FFIType.ptr,
|
|
121
|
+
},
|
|
122
|
+
zfull_response_headers_len: {
|
|
123
|
+
args: [FFIType.ptr],
|
|
124
|
+
returns: FFIType.u64,
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// =========================================================================
|
|
128
|
+
// CACHE FFI
|
|
129
|
+
// =========================================================================
|
|
130
|
+
|
|
131
|
+
zcache_new: {
|
|
132
|
+
args: [FFIType.u64], // l1_capacity
|
|
133
|
+
returns: FFIType.ptr,
|
|
134
|
+
},
|
|
135
|
+
zcache_free: {
|
|
136
|
+
args: [FFIType.ptr],
|
|
137
|
+
returns: FFIType.void,
|
|
138
|
+
},
|
|
139
|
+
zcache_get: {
|
|
140
|
+
args: [FFIType.ptr, FFIType.cstring], // cache, key
|
|
141
|
+
returns: FFIType.ptr, // ZCacheEntry*
|
|
142
|
+
},
|
|
143
|
+
zcache_entry_free: {
|
|
144
|
+
args: [FFIType.ptr],
|
|
145
|
+
returns: FFIType.void,
|
|
146
|
+
},
|
|
147
|
+
zcache_set: {
|
|
148
|
+
args: [
|
|
149
|
+
FFIType.ptr, // cache
|
|
150
|
+
FFIType.cstring, // key
|
|
151
|
+
FFIType.u16, // status
|
|
152
|
+
FFIType.ptr, // headers_ptr
|
|
153
|
+
FFIType.u64, // headers_len
|
|
154
|
+
FFIType.ptr, // body_ptr
|
|
155
|
+
FFIType.u64, // body_len
|
|
156
|
+
FFIType.i64, // ttl
|
|
157
|
+
],
|
|
158
|
+
returns: FFIType.void,
|
|
159
|
+
},
|
|
160
|
+
zcache_preload_l1: {
|
|
161
|
+
args: [FFIType.ptr, FFIType.cstring], // cache, key
|
|
162
|
+
returns: FFIType.bool,
|
|
163
|
+
},
|
|
164
|
+
zcache_clear: {
|
|
165
|
+
args: [FFIType.ptr],
|
|
166
|
+
returns: FFIType.void,
|
|
167
|
+
},
|
|
168
|
+
zcache_stats: {
|
|
169
|
+
args: [FFIType.ptr],
|
|
170
|
+
returns: FFIType.ptr, // CacheStats*
|
|
171
|
+
},
|
|
172
|
+
zcache_stats_free: {
|
|
173
|
+
args: [FFIType.ptr],
|
|
174
|
+
returns: FFIType.void,
|
|
175
|
+
},
|
|
84
176
|
} as const;
|
|
85
177
|
|
|
86
178
|
export const lib = dlopen(libPath, ffiDef);
|
package/components/types.ts
CHANGED
|
@@ -90,14 +90,16 @@ export interface ParsedUrl {
|
|
|
90
90
|
path: string;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
export interface
|
|
93
|
+
/** Target URL with a key for type-safe access */
|
|
94
|
+
export interface TargetUrlConfig {
|
|
95
95
|
/** Unique key to access this URL via client.url[key] */
|
|
96
96
|
key: string;
|
|
97
|
-
/** The URL
|
|
97
|
+
/** The target URL */
|
|
98
98
|
url: string;
|
|
99
99
|
/** Enable response caching for this URL (default: false) */
|
|
100
100
|
cache?: boolean | CacheConfig;
|
|
101
|
+
/** Enable anti-bot protection for all requests to this URL (default: false) */
|
|
102
|
+
antibot?: boolean;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** Cache configuration */
|
|
@@ -181,6 +183,104 @@ export interface UrlEndpoint {
|
|
|
181
183
|
prefetch(options?: UrlRequestOptions): Promise<void>;
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
/** Type helper to extract keys from
|
|
185
|
-
export type
|
|
186
|
+
/** Type helper to extract keys from target config array */
|
|
187
|
+
export type ExtractTargetKeys<T extends readonly TargetUrlConfig[]> =
|
|
186
188
|
T[number]["key"];
|
|
189
|
+
|
|
190
|
+
/** Context passed to request interceptors */
|
|
191
|
+
export interface InterceptorRequestContext {
|
|
192
|
+
method: string;
|
|
193
|
+
url: string;
|
|
194
|
+
headers: Record<string, string>;
|
|
195
|
+
body?: string | null;
|
|
196
|
+
path?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Context passed to response interceptors */
|
|
200
|
+
export interface InterceptorResponseContext<T = any> {
|
|
201
|
+
request: InterceptorRequestContext;
|
|
202
|
+
response: JirenResponse<T>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Request interceptor - can modify request before sending */
|
|
206
|
+
export type RequestInterceptor = (
|
|
207
|
+
ctx: InterceptorRequestContext
|
|
208
|
+
) => InterceptorRequestContext | Promise<InterceptorRequestContext>;
|
|
209
|
+
|
|
210
|
+
/** Response interceptor - can transform response after receiving */
|
|
211
|
+
export type ResponseInterceptor = <T>(
|
|
212
|
+
ctx: InterceptorResponseContext<T>
|
|
213
|
+
) => InterceptorResponseContext<T> | Promise<InterceptorResponseContext<T>>;
|
|
214
|
+
|
|
215
|
+
/** Error interceptor - handles errors thrown during request */
|
|
216
|
+
export type ErrorInterceptor = (
|
|
217
|
+
error: Error,
|
|
218
|
+
ctx: InterceptorRequestContext
|
|
219
|
+
) => void | Promise<void>;
|
|
220
|
+
|
|
221
|
+
/** Interceptor configuration */
|
|
222
|
+
export interface Interceptors {
|
|
223
|
+
request?: RequestInterceptor[];
|
|
224
|
+
response?: ResponseInterceptor[];
|
|
225
|
+
error?: ErrorInterceptor[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Metrics for a single endpoint */
|
|
229
|
+
export interface EndpointMetrics {
|
|
230
|
+
endpoint: string;
|
|
231
|
+
requests: {
|
|
232
|
+
total: number;
|
|
233
|
+
success: number;
|
|
234
|
+
failed: number;
|
|
235
|
+
};
|
|
236
|
+
statusCodes: Record<number, number>;
|
|
237
|
+
timing: {
|
|
238
|
+
avgMs: number;
|
|
239
|
+
minMs: number;
|
|
240
|
+
maxMs: number;
|
|
241
|
+
p50Ms: number;
|
|
242
|
+
p95Ms: number;
|
|
243
|
+
p99Ms: number;
|
|
244
|
+
};
|
|
245
|
+
cache: {
|
|
246
|
+
l1Hits: number;
|
|
247
|
+
l1Misses: number;
|
|
248
|
+
l2Hits: number;
|
|
249
|
+
l2Misses: number;
|
|
250
|
+
hitRate: string;
|
|
251
|
+
};
|
|
252
|
+
deduplication: {
|
|
253
|
+
hits: number;
|
|
254
|
+
misses: number;
|
|
255
|
+
hitRate: string;
|
|
256
|
+
};
|
|
257
|
+
bytes: {
|
|
258
|
+
sent: number;
|
|
259
|
+
received: number;
|
|
260
|
+
};
|
|
261
|
+
errors: Record<string, number>;
|
|
262
|
+
lastRequestAt: number | null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Global aggregated metrics across all endpoints */
|
|
266
|
+
export interface GlobalMetrics {
|
|
267
|
+
totalRequests: number;
|
|
268
|
+
totalSuccess: number;
|
|
269
|
+
totalFailed: number;
|
|
270
|
+
avgResponseTimeMs: number;
|
|
271
|
+
totalBytesSent: number;
|
|
272
|
+
totalBytesReceived: number;
|
|
273
|
+
overallCacheHitRate: string;
|
|
274
|
+
overallDeduplicationRate: string;
|
|
275
|
+
endpoints: number;
|
|
276
|
+
uptime: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Public API for accessing metrics */
|
|
280
|
+
export interface MetricsAPI {
|
|
281
|
+
get(endpoint: string): EndpointMetrics | null;
|
|
282
|
+
getAll(): Record<string, EndpointMetrics>;
|
|
283
|
+
getGlobal(): GlobalMetrics;
|
|
284
|
+
reset(endpoint?: string): void;
|
|
285
|
+
export(): string;
|
|
286
|
+
}
|
package/lib/libhttpclient.dylib
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,440 +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 cache: ResponseCache;
|
|
62
|
-
|
|
63
|
-
/** Type-safe URL accessor for warmed-up URLs */
|
|
64
|
-
public readonly url: UrlAccessor<T>;
|
|
65
|
-
|
|
66
|
-
constructor(options?: JirenClientOptions<T>) {
|
|
67
|
-
// Initialize cache
|
|
68
|
-
this.cache = new ResponseCache(100);
|
|
69
|
-
|
|
70
|
-
// Process warmup URLs
|
|
71
|
-
if (options?.warmup) {
|
|
72
|
-
const urls: string[] = [];
|
|
73
|
-
const warmup = options.warmup;
|
|
74
|
-
|
|
75
|
-
if (Array.isArray(warmup)) {
|
|
76
|
-
for (const item of warmup) {
|
|
77
|
-
if (typeof item === "string") {
|
|
78
|
-
urls.push(item);
|
|
79
|
-
} else {
|
|
80
|
-
// WarmupUrlConfig with key and optional cache
|
|
81
|
-
const config = item as WarmupUrlConfig;
|
|
82
|
-
urls.push(config.url);
|
|
83
|
-
this.urlMap.set(config.key, config.url);
|
|
84
|
-
|
|
85
|
-
// Store cache config
|
|
86
|
-
if (config.cache) {
|
|
87
|
-
const cacheConfig =
|
|
88
|
-
typeof config.cache === "boolean"
|
|
89
|
-
? { enabled: true, ttl: 60000 }
|
|
90
|
-
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
91
|
-
this.cacheConfig.set(config.key, cacheConfig);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
// Record<string, UrlConfig>
|
|
97
|
-
for (const [key, urlConfig] of Object.entries(warmup)) {
|
|
98
|
-
if (typeof urlConfig === "string") {
|
|
99
|
-
// Simple string URL
|
|
100
|
-
urls.push(urlConfig);
|
|
101
|
-
this.urlMap.set(key, urlConfig);
|
|
102
|
-
} else {
|
|
103
|
-
// URL config object with cache
|
|
104
|
-
urls.push(urlConfig.url);
|
|
105
|
-
this.urlMap.set(key, urlConfig.url);
|
|
106
|
-
|
|
107
|
-
// Store cache config
|
|
108
|
-
if (urlConfig.cache) {
|
|
109
|
-
const cacheConfig =
|
|
110
|
-
typeof urlConfig.cache === "boolean"
|
|
111
|
-
? { enabled: true, ttl: 60000 }
|
|
112
|
-
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
113
|
-
this.cacheConfig.set(key, cacheConfig);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (urls.length > 0) {
|
|
120
|
-
this.warmup(urls);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Create proxy for type-safe URL access
|
|
125
|
-
this.url = this.createUrlAccessor();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Creates a proxy-based URL accessor for type-safe access.
|
|
130
|
-
*/
|
|
131
|
-
private createUrlAccessor(): UrlAccessor<T> {
|
|
132
|
-
const self = this;
|
|
133
|
-
|
|
134
|
-
return new Proxy({} as UrlAccessor<T>, {
|
|
135
|
-
get(_target, prop: string) {
|
|
136
|
-
const baseUrl = self.urlMap.get(prop);
|
|
137
|
-
if (!baseUrl) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`URL key "${prop}" not found. Available keys: ${Array.from(
|
|
140
|
-
self.urlMap.keys()
|
|
141
|
-
).join(", ")}`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const buildUrl = (path?: string) =>
|
|
146
|
-
path
|
|
147
|
-
? `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`
|
|
148
|
-
: baseUrl;
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
get: async <R = any>(
|
|
152
|
-
options?: UrlRequestOptions
|
|
153
|
-
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
154
|
-
const cacheConfig = self.cacheConfig.get(prop);
|
|
155
|
-
|
|
156
|
-
if (cacheConfig?.enabled) {
|
|
157
|
-
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
158
|
-
if (cached) return cached as any;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const response = await self.request<R>(
|
|
162
|
-
"GET",
|
|
163
|
-
buildUrl(options?.path),
|
|
164
|
-
null,
|
|
165
|
-
{
|
|
166
|
-
headers: options?.headers,
|
|
167
|
-
maxRedirects: options?.maxRedirects,
|
|
168
|
-
responseType: options?.responseType,
|
|
169
|
-
antibot: options?.antibot,
|
|
170
|
-
}
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
cacheConfig?.enabled &&
|
|
175
|
-
typeof response === "object" &&
|
|
176
|
-
"status" in response
|
|
177
|
-
) {
|
|
178
|
-
self.cache.set(
|
|
179
|
-
baseUrl,
|
|
180
|
-
response as JirenResponse,
|
|
181
|
-
cacheConfig.ttl,
|
|
182
|
-
options?.path,
|
|
183
|
-
options
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return response;
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
post: async <R = any>(
|
|
191
|
-
body?: string | null,
|
|
192
|
-
options?: UrlRequestOptions
|
|
193
|
-
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
194
|
-
return self.request<R>(
|
|
195
|
-
"POST",
|
|
196
|
-
buildUrl(options?.path),
|
|
197
|
-
body || null,
|
|
198
|
-
{
|
|
199
|
-
headers: options?.headers,
|
|
200
|
-
maxRedirects: options?.maxRedirects,
|
|
201
|
-
responseType: options?.responseType,
|
|
202
|
-
}
|
|
203
|
-
);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
put: async <R = any>(
|
|
207
|
-
body?: string | null,
|
|
208
|
-
options?: UrlRequestOptions
|
|
209
|
-
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
210
|
-
return self.request<R>(
|
|
211
|
-
"PUT",
|
|
212
|
-
buildUrl(options?.path),
|
|
213
|
-
body || null,
|
|
214
|
-
{
|
|
215
|
-
headers: options?.headers,
|
|
216
|
-
maxRedirects: options?.maxRedirects,
|
|
217
|
-
responseType: options?.responseType,
|
|
218
|
-
}
|
|
219
|
-
);
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
patch: async <R = any>(
|
|
223
|
-
body?: string | null,
|
|
224
|
-
options?: UrlRequestOptions
|
|
225
|
-
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
226
|
-
return self.request<R>(
|
|
227
|
-
"PATCH",
|
|
228
|
-
buildUrl(options?.path),
|
|
229
|
-
body || null,
|
|
230
|
-
{
|
|
231
|
-
headers: options?.headers,
|
|
232
|
-
maxRedirects: options?.maxRedirects,
|
|
233
|
-
responseType: options?.responseType,
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
delete: async <R = any>(
|
|
239
|
-
body?: string | null,
|
|
240
|
-
options?: UrlRequestOptions
|
|
241
|
-
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
242
|
-
return self.request<R>(
|
|
243
|
-
"DELETE",
|
|
244
|
-
buildUrl(options?.path),
|
|
245
|
-
body || null,
|
|
246
|
-
{
|
|
247
|
-
headers: options?.headers,
|
|
248
|
-
maxRedirects: options?.maxRedirects,
|
|
249
|
-
responseType: options?.responseType,
|
|
250
|
-
}
|
|
251
|
-
);
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
head: async (
|
|
255
|
-
options?: UrlRequestOptions
|
|
256
|
-
): Promise<JirenResponse<any>> => {
|
|
257
|
-
return self.request("HEAD", buildUrl(options?.path), null, {
|
|
258
|
-
headers: options?.headers,
|
|
259
|
-
maxRedirects: options?.maxRedirects,
|
|
260
|
-
antibot: options?.antibot,
|
|
261
|
-
});
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
options: async (
|
|
265
|
-
options?: UrlRequestOptions
|
|
266
|
-
): Promise<JirenResponse<any>> => {
|
|
267
|
-
return self.request("OPTIONS", buildUrl(options?.path), null, {
|
|
268
|
-
headers: options?.headers,
|
|
269
|
-
maxRedirects: options?.maxRedirects,
|
|
270
|
-
antibot: options?.antibot,
|
|
271
|
-
});
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
prefetch: async (options?: UrlRequestOptions): Promise<void> => {
|
|
275
|
-
self.cache.clear(baseUrl);
|
|
276
|
-
const cacheConfig = self.cacheConfig.get(prop);
|
|
277
|
-
if (cacheConfig?.enabled) {
|
|
278
|
-
await self.request("GET", buildUrl(options?.path), null, {
|
|
279
|
-
headers: options?.headers,
|
|
280
|
-
maxRedirects: options?.maxRedirects,
|
|
281
|
-
antibot: options?.antibot,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
} as UrlEndpoint;
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
public close(): void {
|
|
291
|
-
// No-op for fetch client
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
public async warmup(urls: string[]): Promise<void> {
|
|
295
|
-
// Basic prefetch for Node.js fallback (HEAD request)
|
|
296
|
-
await Promise.allSettled(
|
|
297
|
-
urls.map((url) =>
|
|
298
|
-
fetch(url, { method: "HEAD", signal: AbortSignal.timeout(2000) }).catch(
|
|
299
|
-
() => {}
|
|
300
|
-
)
|
|
301
|
-
)
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* @deprecated Use warmup() instead
|
|
307
|
-
*/
|
|
308
|
-
public prefetch(urls: string[]): void {
|
|
309
|
-
this.warmup(urls);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
public async request<T = any>(
|
|
313
|
-
method: string,
|
|
314
|
-
url: string,
|
|
315
|
-
body?: string | null,
|
|
316
|
-
options?: RequestOptions & { responseType: "json" }
|
|
317
|
-
): Promise<T>;
|
|
318
|
-
public async request<T = any>(
|
|
319
|
-
method: string,
|
|
320
|
-
url: string,
|
|
321
|
-
body?: string | null,
|
|
322
|
-
options?: RequestOptions & { responseType: "text" }
|
|
323
|
-
): Promise<string>;
|
|
324
|
-
public async request<T = any>(
|
|
325
|
-
method: string,
|
|
326
|
-
url: string,
|
|
327
|
-
body?: string | null,
|
|
328
|
-
options?: RequestOptions & { responseType: "arraybuffer" }
|
|
329
|
-
): Promise<ArrayBuffer>;
|
|
330
|
-
public async request<T = any>(
|
|
331
|
-
method: string,
|
|
332
|
-
url: string,
|
|
333
|
-
body?: string | null,
|
|
334
|
-
options?: RequestOptions & { responseType: "blob" }
|
|
335
|
-
): Promise<Blob>;
|
|
336
|
-
public async request<T = any>(
|
|
337
|
-
method: string,
|
|
338
|
-
url: string,
|
|
339
|
-
body?: string | null,
|
|
340
|
-
options?: RequestOptions
|
|
341
|
-
): Promise<JirenResponse<T>>;
|
|
342
|
-
public async request<T = any>(
|
|
343
|
-
method: string,
|
|
344
|
-
url: string,
|
|
345
|
-
body?: string | null,
|
|
346
|
-
options?: RequestOptions | Record<string, string> | null
|
|
347
|
-
): Promise<JirenResponse<T> | T | string | ArrayBuffer | Blob> {
|
|
348
|
-
// Normalize options
|
|
349
|
-
let headers: Record<string, string> = {};
|
|
350
|
-
let responseType: RequestOptions["responseType"] | undefined;
|
|
351
|
-
|
|
352
|
-
if (options) {
|
|
353
|
-
if (
|
|
354
|
-
"maxRedirects" in options ||
|
|
355
|
-
"headers" in options ||
|
|
356
|
-
"responseType" in options ||
|
|
357
|
-
"method" in options ||
|
|
358
|
-
"timeout" in options ||
|
|
359
|
-
"antibot" in options
|
|
360
|
-
) {
|
|
361
|
-
const opts = options as RequestOptions;
|
|
362
|
-
if (opts.headers) headers = opts.headers;
|
|
363
|
-
if (opts.responseType) responseType = opts.responseType;
|
|
364
|
-
} else {
|
|
365
|
-
headers = options as Record<string, string>;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const fetchOptions: RequestInit = {
|
|
370
|
-
method,
|
|
371
|
-
headers: headers,
|
|
372
|
-
body: body || undefined,
|
|
373
|
-
redirect: "follow", // Native client follows redirects
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
const response = await fetch(url, fetchOptions);
|
|
377
|
-
|
|
378
|
-
// Auto-parse if requested
|
|
379
|
-
if (responseType) {
|
|
380
|
-
if (responseType === "json") return response.json() as Promise<T>;
|
|
381
|
-
if (responseType === "text") return response.text();
|
|
382
|
-
if (responseType === "arraybuffer") return response.arrayBuffer();
|
|
383
|
-
if (responseType === "blob") return response.blob();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Construct JirenResponse
|
|
387
|
-
const responseHeaders: Record<string, string> = {};
|
|
388
|
-
response.headers.forEach((val, key) => {
|
|
389
|
-
responseHeaders[key] = val;
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Create a clone for each reading method to avoid "body used" error if user reads multiple times
|
|
393
|
-
// However, fetch body can only be read once. We replicate the behavior of the native client
|
|
394
|
-
// where we can read it. To do that we might need to buffer it OR just expose the raw response.
|
|
395
|
-
// The native client implementation buffers the whole body. Let's match that.
|
|
396
|
-
|
|
397
|
-
const buffer = await response.arrayBuffer();
|
|
398
|
-
|
|
399
|
-
let bodyUsed = false;
|
|
400
|
-
const consumeBody = () => {
|
|
401
|
-
bodyUsed = true;
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
const bodyObj: JirenResponseBody<T> = {
|
|
405
|
-
bodyUsed: false,
|
|
406
|
-
arrayBuffer: async () => {
|
|
407
|
-
consumeBody();
|
|
408
|
-
return buffer.slice(0);
|
|
409
|
-
},
|
|
410
|
-
blob: async () => {
|
|
411
|
-
consumeBody();
|
|
412
|
-
return new Blob([buffer]);
|
|
413
|
-
},
|
|
414
|
-
text: async () => {
|
|
415
|
-
consumeBody();
|
|
416
|
-
return new TextDecoder().decode(buffer);
|
|
417
|
-
},
|
|
418
|
-
json: async <R = T>(): Promise<R> => {
|
|
419
|
-
consumeBody();
|
|
420
|
-
const text = new TextDecoder().decode(buffer);
|
|
421
|
-
return JSON.parse(text);
|
|
422
|
-
},
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
426
|
-
get: () => bodyUsed,
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
url: response.url,
|
|
431
|
-
status: response.status,
|
|
432
|
-
statusText: response.statusText,
|
|
433
|
-
headers: responseHeaders,
|
|
434
|
-
ok: response.ok,
|
|
435
|
-
redirected: response.redirected,
|
|
436
|
-
type: response.type,
|
|
437
|
-
body: bodyObj,
|
|
438
|
-
} as JirenResponse<T>;
|
|
439
|
-
}
|
|
440
|
-
}
|