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