jiren 1.3.1 → 1.4.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.
- package/README.md +52 -0
- package/components/cache.ts +398 -41
- package/components/client-node-native.ts +56 -77
- package/components/client-node.ts +34 -1
- package/components/client.ts +138 -77
- package/components/index.ts +6 -0
- package/components/native-cache.ts +181 -0
- package/components/native-node.ts +26 -0
- package/components/native.ts +50 -0
- package/components/types.ts +40 -0
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +6 -14
- package/dist/index-node.js +0 -616
- package/dist/index.js +0 -712
- package/lib/libcurl-impersonate.4.dylib +0 -0
|
@@ -77,6 +77,7 @@ export class JirenClient<
|
|
|
77
77
|
private urlMap: Map<string, string> = new Map();
|
|
78
78
|
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
79
79
|
new Map();
|
|
80
|
+
private antibotConfig: Map<string, boolean> = new Map();
|
|
80
81
|
private cache: ResponseCache;
|
|
81
82
|
|
|
82
83
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
@@ -117,6 +118,11 @@ export class JirenClient<
|
|
|
117
118
|
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
118
119
|
this.cacheConfig.set(config.key, cacheConfig);
|
|
119
120
|
}
|
|
121
|
+
|
|
122
|
+
// Store antibot config
|
|
123
|
+
if (config.antibot) {
|
|
124
|
+
this.antibotConfig.set(config.key, true);
|
|
125
|
+
}
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
} else {
|
|
@@ -139,6 +145,11 @@ export class JirenClient<
|
|
|
139
145
|
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
140
146
|
this.cacheConfig.set(key, cacheConfig);
|
|
141
147
|
}
|
|
148
|
+
|
|
149
|
+
// Store antibot config
|
|
150
|
+
if ((urlConfig as { antibot?: boolean }).antibot) {
|
|
151
|
+
this.antibotConfig.set(key, true);
|
|
152
|
+
}
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
@@ -146,12 +157,30 @@ export class JirenClient<
|
|
|
146
157
|
if (urls.length > 0) {
|
|
147
158
|
this.warmup(urls);
|
|
148
159
|
}
|
|
160
|
+
|
|
161
|
+
// Preload L2 disk cache entries into L1 memory for cached endpoints
|
|
162
|
+
// This ensures user's first request hits L1 (~0.001ms) instead of L2 (~5ms)
|
|
163
|
+
for (const [key, config] of this.cacheConfig.entries()) {
|
|
164
|
+
if (config.enabled) {
|
|
165
|
+
const url = this.urlMap.get(key);
|
|
166
|
+
if (url) {
|
|
167
|
+
this.cache.preloadL1(url);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
// Create proxy for type-safe URL access
|
|
152
174
|
this.url = this.createUrlAccessor();
|
|
153
175
|
}
|
|
154
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Wait for warmup to complete
|
|
179
|
+
*/
|
|
180
|
+
public async waitForWarmup(): Promise<void> {
|
|
181
|
+
// Native warmup is synchronous, so this is effectively a no-op
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
/**
|
|
156
185
|
* Creates a proxy-based URL accessor for type-safe access.
|
|
157
186
|
*/
|
|
@@ -180,10 +209,14 @@ export class JirenClient<
|
|
|
180
209
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
181
210
|
const cacheConfig = self.cacheConfig.get(prop);
|
|
182
211
|
|
|
212
|
+
// Check if antibot is enabled for this URL (from warmup config or per-request)
|
|
213
|
+
const useAntibot =
|
|
214
|
+
options?.antibot ?? self.antibotConfig.get(prop) ?? false;
|
|
215
|
+
|
|
183
216
|
if (cacheConfig?.enabled) {
|
|
184
217
|
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
185
218
|
if (cached) {
|
|
186
|
-
return
|
|
219
|
+
return cached as any;
|
|
187
220
|
}
|
|
188
221
|
}
|
|
189
222
|
|
|
@@ -195,7 +228,7 @@ export class JirenClient<
|
|
|
195
228
|
headers: options?.headers,
|
|
196
229
|
maxRedirects: options?.maxRedirects,
|
|
197
230
|
responseType: options?.responseType,
|
|
198
|
-
antibot:
|
|
231
|
+
antibot: useAntibot,
|
|
199
232
|
}
|
|
200
233
|
);
|
|
201
234
|
|
|
@@ -535,39 +568,42 @@ export class JirenClient<
|
|
|
535
568
|
// Copy body content
|
|
536
569
|
buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
|
|
537
570
|
|
|
538
|
-
// Handle GZIP compression
|
|
571
|
+
// Handle GZIP compression - search for magic bytes in first 16 bytes
|
|
572
|
+
// (handles chunked encoding or other framing that may add prefix bytes)
|
|
539
573
|
const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
|
|
574
|
+
let gzipOffset = -1;
|
|
540
575
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
(buffer
|
|
544
|
-
|
|
576
|
+
// Search for gzip magic bytes (0x1f 0x8b) in first 16 bytes
|
|
577
|
+
for (let i = 0; i < Math.min(16, buffer.length - 1); i++) {
|
|
578
|
+
if (buffer[i] === 0x1f && buffer[i + 1] === 0x8b) {
|
|
579
|
+
gzipOffset = i;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (contentEncoding === "gzip" || gzipOffset >= 0) {
|
|
545
585
|
try {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
console.log(
|
|
586
|
+
// If we found gzip at an offset, slice from there
|
|
587
|
+
const gzipData = gzipOffset > 0 ? buffer.slice(gzipOffset) : buffer;
|
|
588
|
+
console.log(
|
|
589
|
+
`[Jiren] Decompressing gzip (offset: ${
|
|
590
|
+
gzipOffset >= 0 ? gzipOffset : 0
|
|
591
|
+
}, size: ${gzipData.length})`
|
|
592
|
+
);
|
|
593
|
+
buffer = zlib.gunzipSync(gzipData);
|
|
549
594
|
} catch (e) {
|
|
550
595
|
console.warn("Failed to gunzip response body:", e);
|
|
551
596
|
}
|
|
552
597
|
}
|
|
553
598
|
}
|
|
554
599
|
|
|
555
|
-
// Convert to base64 for caching persistence
|
|
556
|
-
let base64Data = "";
|
|
557
|
-
try {
|
|
558
|
-
base64Data = buffer.toString("base64");
|
|
559
|
-
} catch (e) {
|
|
560
|
-
// Should not happen
|
|
561
|
-
}
|
|
562
|
-
|
|
563
600
|
let bodyUsed = false;
|
|
564
601
|
const consumeBody = () => {
|
|
565
602
|
bodyUsed = true;
|
|
566
603
|
};
|
|
567
604
|
|
|
568
|
-
const bodyObj: JirenResponseBody<T>
|
|
605
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
569
606
|
bodyUsed: false,
|
|
570
|
-
_raw: base64Data,
|
|
571
607
|
arrayBuffer: async () => {
|
|
572
608
|
consumeBody();
|
|
573
609
|
return buffer.buffer.slice(
|
|
@@ -607,63 +643,6 @@ export class JirenClient<
|
|
|
607
643
|
lib.symbols.zclient_response_free(respPtr);
|
|
608
644
|
}
|
|
609
645
|
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* Rehydrates a cached response object by restoring the body interface methods.
|
|
613
|
-
* This is necessary because JSON serialization strips functions.
|
|
614
|
-
*/
|
|
615
|
-
private rehydrateResponse(cached: any): JirenResponse {
|
|
616
|
-
// If it already has methods, return as is
|
|
617
|
-
if (typeof cached.body.text === "function") return cached;
|
|
618
|
-
|
|
619
|
-
// Retrieve raw data
|
|
620
|
-
const rawData = cached.body._raw;
|
|
621
|
-
let buffer: Buffer;
|
|
622
|
-
|
|
623
|
-
if (rawData) {
|
|
624
|
-
buffer = Buffer.from(rawData, "base64");
|
|
625
|
-
} else {
|
|
626
|
-
buffer = Buffer.from("");
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
let bodyUsed = cached.body.bodyUsed || false;
|
|
630
|
-
const consumeBody = () => {
|
|
631
|
-
bodyUsed = true;
|
|
632
|
-
};
|
|
633
|
-
|
|
634
|
-
const bodyObj: JirenResponseBody = {
|
|
635
|
-
_raw: rawData, // Keep it for re-caching if needed
|
|
636
|
-
bodyUsed,
|
|
637
|
-
arrayBuffer: async () => {
|
|
638
|
-
consumeBody();
|
|
639
|
-
return buffer.buffer.slice(
|
|
640
|
-
buffer.byteOffset,
|
|
641
|
-
buffer.byteOffset + buffer.byteLength
|
|
642
|
-
) as ArrayBuffer;
|
|
643
|
-
},
|
|
644
|
-
blob: async () => {
|
|
645
|
-
consumeBody();
|
|
646
|
-
return new Blob([buffer as any]);
|
|
647
|
-
},
|
|
648
|
-
text: async () => {
|
|
649
|
-
consumeBody();
|
|
650
|
-
return buffer.toString("utf-8");
|
|
651
|
-
},
|
|
652
|
-
json: async <R = any>(): Promise<R> => {
|
|
653
|
-
consumeBody();
|
|
654
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
655
|
-
},
|
|
656
|
-
} as any;
|
|
657
|
-
|
|
658
|
-
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
659
|
-
get: () => bodyUsed,
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
return {
|
|
663
|
-
...cached,
|
|
664
|
-
body: bodyObj,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
646
|
}
|
|
668
647
|
|
|
669
648
|
class NativeHeaders {
|
|
@@ -58,6 +58,7 @@ export class JirenClient<
|
|
|
58
58
|
private urlMap: Map<string, string> = new Map();
|
|
59
59
|
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
60
60
|
new Map();
|
|
61
|
+
private antibotConfig: Map<string, boolean> = new Map();
|
|
61
62
|
private cache: ResponseCache;
|
|
62
63
|
|
|
63
64
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
@@ -90,6 +91,11 @@ export class JirenClient<
|
|
|
90
91
|
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
91
92
|
this.cacheConfig.set(config.key, cacheConfig);
|
|
92
93
|
}
|
|
94
|
+
|
|
95
|
+
// Store antibot config
|
|
96
|
+
if (config.antibot) {
|
|
97
|
+
this.antibotConfig.set(config.key, true);
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
} else {
|
|
@@ -112,6 +118,11 @@ export class JirenClient<
|
|
|
112
118
|
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
113
119
|
this.cacheConfig.set(key, cacheConfig);
|
|
114
120
|
}
|
|
121
|
+
|
|
122
|
+
// Store antibot config
|
|
123
|
+
if ((urlConfig as { antibot?: boolean }).antibot) {
|
|
124
|
+
this.antibotConfig.set(key, true);
|
|
125
|
+
}
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
128
|
}
|
|
@@ -119,12 +130,30 @@ export class JirenClient<
|
|
|
119
130
|
if (urls.length > 0) {
|
|
120
131
|
this.warmup(urls);
|
|
121
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
|
+
}
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
// Create proxy for type-safe URL access
|
|
125
147
|
this.url = this.createUrlAccessor();
|
|
126
148
|
}
|
|
127
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
|
+
|
|
128
157
|
/**
|
|
129
158
|
* Creates a proxy-based URL accessor for type-safe access.
|
|
130
159
|
*/
|
|
@@ -153,6 +182,10 @@ export class JirenClient<
|
|
|
153
182
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
154
183
|
const cacheConfig = self.cacheConfig.get(prop);
|
|
155
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
|
+
|
|
156
189
|
if (cacheConfig?.enabled) {
|
|
157
190
|
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
158
191
|
if (cached) return cached as any;
|
|
@@ -166,7 +199,7 @@ export class JirenClient<
|
|
|
166
199
|
headers: options?.headers,
|
|
167
200
|
maxRedirects: options?.maxRedirects,
|
|
168
201
|
responseType: options?.responseType,
|
|
169
|
-
antibot:
|
|
202
|
+
antibot: useAntibot,
|
|
170
203
|
}
|
|
171
204
|
);
|
|
172
205
|
|
package/components/client.ts
CHANGED
|
@@ -10,6 +10,12 @@ import type {
|
|
|
10
10
|
UrlEndpoint,
|
|
11
11
|
CacheConfig,
|
|
12
12
|
RetryConfig,
|
|
13
|
+
Interceptors,
|
|
14
|
+
RequestInterceptor,
|
|
15
|
+
ResponseInterceptor,
|
|
16
|
+
ErrorInterceptor,
|
|
17
|
+
InterceptorRequestContext,
|
|
18
|
+
InterceptorResponseContext,
|
|
13
19
|
} from "./types";
|
|
14
20
|
|
|
15
21
|
const STATUS_TEXT: Record<number, string> = {
|
|
@@ -27,8 +33,10 @@ const STATUS_TEXT: Record<number, string> = {
|
|
|
27
33
|
503: "Service Unavailable",
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
/** URL configuration with optional cache */
|
|
31
|
-
export type UrlConfig =
|
|
36
|
+
/** URL configuration with optional cache and antibot */
|
|
37
|
+
export type UrlConfig =
|
|
38
|
+
| string
|
|
39
|
+
| { url: string; cache?: boolean | CacheConfig; antibot?: boolean };
|
|
32
40
|
|
|
33
41
|
/** Options for JirenClient constructor */
|
|
34
42
|
export interface JirenClientOptions<
|
|
@@ -44,6 +52,9 @@ export interface JirenClientOptions<
|
|
|
44
52
|
|
|
45
53
|
/** Global retry configuration */
|
|
46
54
|
retry?: number | RetryConfig;
|
|
55
|
+
|
|
56
|
+
/** Request/response interceptors */
|
|
57
|
+
interceptors?: Interceptors;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/** Helper to extract keys from Warmup Config */
|
|
@@ -96,9 +107,15 @@ export class JirenClient<
|
|
|
96
107
|
private urlMap: Map<string, string> = new Map();
|
|
97
108
|
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
98
109
|
new Map();
|
|
110
|
+
private antibotConfig: Map<string, boolean> = new Map();
|
|
99
111
|
private cache: ResponseCache;
|
|
100
112
|
private inflightRequests: Map<string, Promise<any>> = new Map();
|
|
101
113
|
private globalRetry?: RetryConfig;
|
|
114
|
+
private requestInterceptors: RequestInterceptor[] = [];
|
|
115
|
+
private responseInterceptors: ResponseInterceptor[] = [];
|
|
116
|
+
private errorInterceptors: ErrorInterceptor[] = [];
|
|
117
|
+
private warmupPromise: Promise<void> | null = null;
|
|
118
|
+
private warmupComplete: Set<string> = new Set();
|
|
102
119
|
|
|
103
120
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
104
121
|
public readonly url: UrlAccessor<T>;
|
|
@@ -138,6 +155,11 @@ export class JirenClient<
|
|
|
138
155
|
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
139
156
|
this.cacheConfig.set(config.key, cacheConfig);
|
|
140
157
|
}
|
|
158
|
+
|
|
159
|
+
// Store antibot config
|
|
160
|
+
if (config.antibot) {
|
|
161
|
+
this.antibotConfig.set(config.key, true);
|
|
162
|
+
}
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
165
|
} else {
|
|
@@ -160,12 +182,31 @@ export class JirenClient<
|
|
|
160
182
|
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
161
183
|
this.cacheConfig.set(key, cacheConfig);
|
|
162
184
|
}
|
|
185
|
+
|
|
186
|
+
// Store antibot config
|
|
187
|
+
if ((urlConfig as { antibot?: boolean }).antibot) {
|
|
188
|
+
this.antibotConfig.set(key, true);
|
|
189
|
+
}
|
|
163
190
|
}
|
|
164
191
|
}
|
|
165
192
|
}
|
|
166
193
|
|
|
167
194
|
if (urls.length > 0) {
|
|
168
|
-
|
|
195
|
+
// Lazy warmup in background (always - it's faster)
|
|
196
|
+
this.warmupPromise = this.warmup(urls).then(() => {
|
|
197
|
+
urls.forEach((url) => this.warmupComplete.add(url));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Preload L2 disk cache entries into L1 memory for cached endpoints
|
|
202
|
+
// This ensures user's first request hits L1 (~0.001ms) instead of L2 (~5ms)
|
|
203
|
+
for (const [key, config] of this.cacheConfig.entries()) {
|
|
204
|
+
if (config.enabled) {
|
|
205
|
+
const url = this.urlMap.get(key);
|
|
206
|
+
if (url) {
|
|
207
|
+
this.cache.preloadL1(url);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
|
|
@@ -179,12 +220,27 @@ export class JirenClient<
|
|
|
179
220
|
? { count: options.retry, delay: 100, backoff: 2 }
|
|
180
221
|
: options.retry;
|
|
181
222
|
}
|
|
223
|
+
|
|
224
|
+
// Initialize interceptors
|
|
225
|
+
if (options?.interceptors) {
|
|
226
|
+
this.requestInterceptors = options.interceptors.request || [];
|
|
227
|
+
this.responseInterceptors = options.interceptors.response || [];
|
|
228
|
+
this.errorInterceptors = options.interceptors.error || [];
|
|
229
|
+
}
|
|
182
230
|
}
|
|
183
231
|
|
|
184
232
|
private async waitFor(ms: number) {
|
|
185
233
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
234
|
}
|
|
187
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Wait for lazy warmup to complete.
|
|
238
|
+
* Only needed if using lazyWarmup: true and want to ensure warmup is done.
|
|
239
|
+
*/
|
|
240
|
+
public async waitForWarmup(): Promise<void> {
|
|
241
|
+
if (this.warmupPromise) await this.warmupPromise;
|
|
242
|
+
}
|
|
243
|
+
|
|
188
244
|
/**
|
|
189
245
|
* Creates a proxy-based URL accessor for type-safe access to warmed-up URLs.
|
|
190
246
|
*/
|
|
@@ -213,14 +269,23 @@ export class JirenClient<
|
|
|
213
269
|
get: async <R = any>(
|
|
214
270
|
options?: UrlRequestOptions
|
|
215
271
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
272
|
+
// Wait for warmup to complete if not yet done
|
|
273
|
+
if (self.warmupPromise && !self.warmupComplete.has(baseUrl)) {
|
|
274
|
+
await self.warmupPromise;
|
|
275
|
+
}
|
|
276
|
+
|
|
216
277
|
// Check if caching is enabled for this URL
|
|
217
278
|
const cacheConfig = self.cacheConfig.get(prop);
|
|
218
279
|
|
|
280
|
+
// Check if antibot is enabled for this URL (from warmup config or per-request)
|
|
281
|
+
const useAntibot =
|
|
282
|
+
options?.antibot ?? self.antibotConfig.get(prop) ?? false;
|
|
283
|
+
|
|
219
284
|
if (cacheConfig?.enabled) {
|
|
220
285
|
// Try to get from cache
|
|
221
286
|
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
222
287
|
if (cached) {
|
|
223
|
-
return
|
|
288
|
+
return cached as any;
|
|
224
289
|
}
|
|
225
290
|
}
|
|
226
291
|
|
|
@@ -247,7 +312,7 @@ export class JirenClient<
|
|
|
247
312
|
headers: options?.headers,
|
|
248
313
|
maxRedirects: options?.maxRedirects,
|
|
249
314
|
responseType: options?.responseType,
|
|
250
|
-
antibot:
|
|
315
|
+
antibot: useAntibot,
|
|
251
316
|
}
|
|
252
317
|
);
|
|
253
318
|
|
|
@@ -409,6 +474,20 @@ export class JirenClient<
|
|
|
409
474
|
}
|
|
410
475
|
}
|
|
411
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Register interceptors dynamically.
|
|
479
|
+
* @param interceptors - Interceptor configuration to add
|
|
480
|
+
* @returns this for chaining
|
|
481
|
+
*/
|
|
482
|
+
public use(interceptors: Interceptors): this {
|
|
483
|
+
if (interceptors.request)
|
|
484
|
+
this.requestInterceptors.push(...interceptors.request);
|
|
485
|
+
if (interceptors.response)
|
|
486
|
+
this.responseInterceptors.push(...interceptors.response);
|
|
487
|
+
if (interceptors.error) this.errorInterceptors.push(...interceptors.error);
|
|
488
|
+
return this;
|
|
489
|
+
}
|
|
490
|
+
|
|
412
491
|
/**
|
|
413
492
|
* Warm up connections to URLs (DNS resolve + QUIC handshake) in parallel.
|
|
414
493
|
* Call this early (e.g., at app startup) so subsequent requests are fast.
|
|
@@ -510,6 +589,20 @@ export class JirenClient<
|
|
|
510
589
|
}
|
|
511
590
|
}
|
|
512
591
|
|
|
592
|
+
// Build interceptor request context
|
|
593
|
+
let ctx: InterceptorRequestContext = { method, url, headers, body };
|
|
594
|
+
|
|
595
|
+
// Run request interceptors
|
|
596
|
+
for (const interceptor of this.requestInterceptors) {
|
|
597
|
+
ctx = await interceptor(ctx);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Apply interceptor modifications
|
|
601
|
+
method = ctx.method;
|
|
602
|
+
url = ctx.url;
|
|
603
|
+
headers = ctx.headers;
|
|
604
|
+
body = ctx.body ?? null;
|
|
605
|
+
|
|
513
606
|
const methodBuffer = Buffer.from(method + "\0");
|
|
514
607
|
const urlBuffer = Buffer.from(url + "\0");
|
|
515
608
|
|
|
@@ -619,6 +712,16 @@ export class JirenClient<
|
|
|
619
712
|
|
|
620
713
|
const response = this.parseResponse<T>(respPtr, url);
|
|
621
714
|
|
|
715
|
+
// Run response interceptors
|
|
716
|
+
let responseCtx: InterceptorResponseContext<T> = {
|
|
717
|
+
request: ctx,
|
|
718
|
+
response,
|
|
719
|
+
};
|
|
720
|
+
for (const interceptor of this.responseInterceptors) {
|
|
721
|
+
responseCtx = await interceptor(responseCtx);
|
|
722
|
+
}
|
|
723
|
+
const finalResponse = responseCtx.response;
|
|
724
|
+
|
|
622
725
|
// Optional: Retry on specific status codes (e.g., 500, 502, 503, 504)
|
|
623
726
|
// For now, we only retry on actual exceptions/network failures (null ptr)
|
|
624
727
|
// or if we decide to throw on 5xx here.
|
|
@@ -626,15 +729,19 @@ export class JirenClient<
|
|
|
626
729
|
|
|
627
730
|
// Auto-parse if requested
|
|
628
731
|
if (responseType) {
|
|
629
|
-
if (responseType === "json") return
|
|
630
|
-
if (responseType === "text") return
|
|
732
|
+
if (responseType === "json") return finalResponse.body.json();
|
|
733
|
+
if (responseType === "text") return finalResponse.body.text();
|
|
631
734
|
if (responseType === "arraybuffer")
|
|
632
|
-
return
|
|
633
|
-
if (responseType === "blob") return
|
|
735
|
+
return finalResponse.body.arrayBuffer();
|
|
736
|
+
if (responseType === "blob") return finalResponse.body.blob();
|
|
634
737
|
}
|
|
635
738
|
|
|
636
|
-
return
|
|
739
|
+
return finalResponse;
|
|
637
740
|
} catch (err) {
|
|
741
|
+
// Run error interceptors
|
|
742
|
+
for (const interceptor of this.errorInterceptors) {
|
|
743
|
+
await interceptor(err as Error, ctx);
|
|
744
|
+
}
|
|
638
745
|
lastError = err;
|
|
639
746
|
if (attempts < maxAttempts) {
|
|
640
747
|
// Wait before retrying
|
|
@@ -697,14 +804,27 @@ export class JirenClient<
|
|
|
697
804
|
if (len > 0 && bodyPtr) {
|
|
698
805
|
// Create a copy of the buffer because the native response is freed immediately after
|
|
699
806
|
buffer = toArrayBuffer(bodyPtr, 0, len).slice(0);
|
|
700
|
-
}
|
|
701
807
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
808
|
+
// Handle GZIP decompression if needed
|
|
809
|
+
const bufferView = new Uint8Array(buffer);
|
|
810
|
+
// Check for gzip magic bytes (0x1f 0x8b)
|
|
811
|
+
if (
|
|
812
|
+
bufferView.length >= 2 &&
|
|
813
|
+
bufferView[0] === 0x1f &&
|
|
814
|
+
bufferView[1] === 0x8b
|
|
815
|
+
) {
|
|
816
|
+
try {
|
|
817
|
+
// Use Bun's built-in gzip decompression
|
|
818
|
+
const decompressed = Bun.gunzipSync(bufferView);
|
|
819
|
+
buffer = decompressed.buffer.slice(
|
|
820
|
+
decompressed.byteOffset,
|
|
821
|
+
decompressed.byteOffset + decompressed.byteLength
|
|
822
|
+
);
|
|
823
|
+
} catch (e) {
|
|
824
|
+
// Decompression failed, keep original buffer
|
|
825
|
+
console.warn("[Jiren] gzip decompression failed:", e);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
708
828
|
}
|
|
709
829
|
|
|
710
830
|
let bodyUsed = false;
|
|
@@ -714,9 +834,8 @@ export class JirenClient<
|
|
|
714
834
|
bodyUsed = true;
|
|
715
835
|
};
|
|
716
836
|
|
|
717
|
-
const bodyObj: JirenResponseBody<T>
|
|
837
|
+
const bodyObj: JirenResponseBody<T> = {
|
|
718
838
|
bodyUsed: false,
|
|
719
|
-
_raw: base64Data,
|
|
720
839
|
arrayBuffer: async () => {
|
|
721
840
|
consumeBody();
|
|
722
841
|
if (Buffer.isBuffer(buffer)) {
|
|
@@ -791,64 +910,6 @@ export class JirenClient<
|
|
|
791
910
|
|
|
792
911
|
return { headers, serializedBody };
|
|
793
912
|
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Rehydrates a cached response object by restoring the body interface methods.
|
|
797
|
-
* This is necessary because JSON serialization strips functions.
|
|
798
|
-
*/
|
|
799
|
-
private rehydrateResponse(cached: any): JirenResponse {
|
|
800
|
-
// If it already has methods, return as is
|
|
801
|
-
if (typeof cached.body.text === "function") return cached;
|
|
802
|
-
|
|
803
|
-
// Retrieve raw data
|
|
804
|
-
const rawData = cached.body._raw;
|
|
805
|
-
let buffer: Buffer;
|
|
806
|
-
|
|
807
|
-
if (rawData) {
|
|
808
|
-
buffer = Buffer.from(rawData, "base64");
|
|
809
|
-
} else {
|
|
810
|
-
buffer = Buffer.from("");
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
let bodyUsed = cached.body.bodyUsed || false;
|
|
814
|
-
const consumeBody = () => {
|
|
815
|
-
bodyUsed = true;
|
|
816
|
-
};
|
|
817
|
-
|
|
818
|
-
const bodyObj: JirenResponseBody = {
|
|
819
|
-
_raw: rawData, // Keep it for re-caching if needed
|
|
820
|
-
bodyUsed,
|
|
821
|
-
arrayBuffer: async () => {
|
|
822
|
-
consumeBody();
|
|
823
|
-
return buffer.buffer.slice(
|
|
824
|
-
buffer.byteOffset,
|
|
825
|
-
buffer.byteOffset + buffer.byteLength
|
|
826
|
-
) as ArrayBuffer;
|
|
827
|
-
},
|
|
828
|
-
blob: async () => {
|
|
829
|
-
consumeBody();
|
|
830
|
-
return new Blob([buffer]);
|
|
831
|
-
},
|
|
832
|
-
text: async () => {
|
|
833
|
-
consumeBody();
|
|
834
|
-
return new TextDecoder().decode(buffer);
|
|
835
|
-
},
|
|
836
|
-
json: async <R = any>(): Promise<R> => {
|
|
837
|
-
consumeBody();
|
|
838
|
-
const text = new TextDecoder().decode(buffer);
|
|
839
|
-
return JSON.parse(text);
|
|
840
|
-
},
|
|
841
|
-
} as any;
|
|
842
|
-
|
|
843
|
-
Object.defineProperty(bodyObj, "bodyUsed", {
|
|
844
|
-
get: () => bodyUsed,
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
return {
|
|
848
|
-
...cached,
|
|
849
|
-
body: bodyObj,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
913
|
}
|
|
853
914
|
|
|
854
915
|
class NativeHeaders {
|
package/components/index.ts
CHANGED
|
@@ -20,6 +20,12 @@ export type {
|
|
|
20
20
|
UrlEndpoint,
|
|
21
21
|
JirenResponse,
|
|
22
22
|
JirenResponseBody,
|
|
23
|
+
Interceptors,
|
|
24
|
+
RequestInterceptor,
|
|
25
|
+
ResponseInterceptor,
|
|
26
|
+
ErrorInterceptor,
|
|
27
|
+
InterceptorRequestContext,
|
|
28
|
+
InterceptorResponseContext,
|
|
23
29
|
} from "./types";
|
|
24
30
|
|
|
25
31
|
// Remove broken exports
|