jiren 1.2.6 → 1.2.7

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,6 +1,7 @@
1
1
  import { nativeLib as lib } from "./native-node";
2
2
  import { ResponseCache } from "./cache";
3
3
  import koffi from "koffi";
4
+ import zlib from "zlib";
4
5
  import type {
5
6
  RequestOptions,
6
7
  JirenResponse,
@@ -533,6 +534,34 @@ export class JirenClient<
533
534
  if (len > 0 && bodyPtr) {
534
535
  // Copy body content
535
536
  buffer = Buffer.from(koffi.decode(bodyPtr, "uint8_t", len));
537
+
538
+ // Handle GZIP compression
539
+ const contentEncoding = headersProxy["content-encoding"]?.toLowerCase();
540
+
541
+ // DEBUG LOGS
542
+ if (len > 0) {
543
+ console.log(`[Jiren] Body len: ${len}, Encoding: ${contentEncoding}`);
544
+ if (buffer.length > 2) {
545
+ console.log(
546
+ `[Jiren] Bytes: 0x${buffer[0]?.toString(
547
+ 16
548
+ )} 0x${buffer[1]?.toString(16)} 0x${buffer[2]?.toString(16)}`
549
+ );
550
+ }
551
+ }
552
+
553
+ if (
554
+ contentEncoding === "gzip" ||
555
+ (buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b)
556
+ ) {
557
+ try {
558
+ console.log("[Jiren] Attempting gunzip...");
559
+ buffer = zlib.gunzipSync(buffer);
560
+ console.log("[Jiren] Gunzip success!");
561
+ } catch (e) {
562
+ console.warn("Failed to gunzip response body:", e);
563
+ }
564
+ }
536
565
  }
537
566
 
538
567
  let bodyUsed = false;
@@ -9,6 +9,7 @@ import type {
9
9
  UrlRequestOptions,
10
10
  UrlEndpoint,
11
11
  CacheConfig,
12
+ RetryConfig,
12
13
  } from "./types";
13
14
 
14
15
  const STATUS_TEXT: Record<number, string> = {
@@ -40,6 +41,9 @@ export interface JirenClientOptions<
40
41
 
41
42
  /** Enable benchmark mode (Force HTTP/2, disable probing) */
42
43
  benchmark?: boolean;
44
+
45
+ /** Global retry configuration */
46
+ retry?: number | RetryConfig;
43
47
  }
44
48
 
45
49
  /** Helper to extract keys from Warmup Config */
@@ -93,6 +97,8 @@ export class JirenClient<
93
97
  private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
94
98
  new Map();
95
99
  private cache: ResponseCache;
100
+ private inflightRequests: Map<string, Promise<any>> = new Map();
101
+ private globalRetry?: RetryConfig;
96
102
 
97
103
  /** Type-safe URL accessor for warmed-up URLs */
98
104
  public readonly url: UrlAccessor<T>;
@@ -165,6 +171,18 @@ export class JirenClient<
165
171
 
166
172
  // Create proxy for type-safe URL access
167
173
  this.url = this.createUrlAccessor();
174
+
175
+ // Store global retry config
176
+ if (options?.retry) {
177
+ this.globalRetry =
178
+ typeof options.retry === "number"
179
+ ? { count: options.retry, delay: 100, backoff: 2 }
180
+ : options.retry;
181
+ }
182
+ }
183
+
184
+ private async waitFor(ms: number) {
185
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
186
  }
169
187
 
170
188
  /**
@@ -206,47 +224,74 @@ export class JirenClient<
206
224
  }
207
225
  }
208
226
 
209
- // Make the request
210
- const response = await self.request<R>(
211
- "GET",
212
- buildUrl(options?.path),
213
- null,
214
- {
215
- headers: options?.headers,
216
- maxRedirects: options?.maxRedirects,
217
- responseType: options?.responseType,
218
- antibot: options?.antibot,
219
- }
220
- );
227
+ // ** Deduplication Logic **
228
+ // Create a unique key for this request based on URL and critical options
229
+ const dedupKey = `GET:${buildUrl(options?.path)}:${JSON.stringify(
230
+ options?.headers || {}
231
+ )}`;
221
232
 
222
- // Store in cache if enabled
223
- if (
224
- cacheConfig?.enabled &&
225
- typeof response === "object" &&
226
- "status" in response
227
- ) {
228
- self.cache.set(
229
- baseUrl,
230
- response as JirenResponse,
231
- cacheConfig.ttl,
232
- options?.path,
233
- options
234
- );
233
+ // Check if there is already an identical request in flight
234
+ if (self.inflightRequests.has(dedupKey)) {
235
+ return self.inflightRequests.get(dedupKey);
235
236
  }
236
237
 
237
- return response;
238
+ // Create the request promise
239
+ const requestPromise = (async () => {
240
+ try {
241
+ // Make the request
242
+ const response = await self.request<R>(
243
+ "GET",
244
+ buildUrl(options?.path),
245
+ null,
246
+ {
247
+ headers: options?.headers,
248
+ maxRedirects: options?.maxRedirects,
249
+ responseType: options?.responseType,
250
+ antibot: options?.antibot,
251
+ }
252
+ );
253
+
254
+ // Store in cache if enabled
255
+ if (
256
+ cacheConfig?.enabled &&
257
+ typeof response === "object" &&
258
+ "status" in response
259
+ ) {
260
+ self.cache.set(
261
+ baseUrl,
262
+ response as JirenResponse,
263
+ cacheConfig.ttl,
264
+ options?.path,
265
+ options
266
+ );
267
+ }
268
+
269
+ return response;
270
+ } finally {
271
+ // Remove from inflight map when done (success or failure)
272
+ self.inflightRequests.delete(dedupKey);
273
+ }
274
+ })();
275
+
276
+ // Store the promise in the map
277
+ self.inflightRequests.set(dedupKey, requestPromise);
278
+
279
+ return requestPromise;
238
280
  },
239
281
 
240
282
  post: async <R = any>(
241
- body?: string | null,
242
283
  options?: UrlRequestOptions
243
284
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
285
+ const { headers, serializedBody } = self.prepareBody(
286
+ options?.body,
287
+ options?.headers
288
+ );
244
289
  return self.request<R>(
245
290
  "POST",
246
291
  buildUrl(options?.path),
247
- body || null,
292
+ serializedBody,
248
293
  {
249
- headers: options?.headers,
294
+ headers,
250
295
  maxRedirects: options?.maxRedirects,
251
296
  responseType: options?.responseType,
252
297
  }
@@ -254,15 +299,18 @@ export class JirenClient<
254
299
  },
255
300
 
256
301
  put: async <R = any>(
257
- body?: string | null,
258
302
  options?: UrlRequestOptions
259
303
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
304
+ const { headers, serializedBody } = self.prepareBody(
305
+ options?.body,
306
+ options?.headers
307
+ );
260
308
  return self.request<R>(
261
309
  "PUT",
262
310
  buildUrl(options?.path),
263
- body || null,
311
+ serializedBody,
264
312
  {
265
- headers: options?.headers,
313
+ headers,
266
314
  maxRedirects: options?.maxRedirects,
267
315
  responseType: options?.responseType,
268
316
  }
@@ -270,15 +318,18 @@ export class JirenClient<
270
318
  },
271
319
 
272
320
  patch: async <R = any>(
273
- body?: string | null,
274
321
  options?: UrlRequestOptions
275
322
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
323
+ const { headers, serializedBody } = self.prepareBody(
324
+ options?.body,
325
+ options?.headers
326
+ );
276
327
  return self.request<R>(
277
328
  "PATCH",
278
329
  buildUrl(options?.path),
279
- body || null,
330
+ serializedBody,
280
331
  {
281
- headers: options?.headers,
332
+ headers,
282
333
  maxRedirects: options?.maxRedirects,
283
334
  responseType: options?.responseType,
284
335
  }
@@ -286,15 +337,18 @@ export class JirenClient<
286
337
  },
287
338
 
288
339
  delete: async <R = any>(
289
- body?: string | null,
290
340
  options?: UrlRequestOptions
291
341
  ): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
342
+ const { headers, serializedBody } = self.prepareBody(
343
+ options?.body,
344
+ options?.headers
345
+ );
292
346
  return self.request<R>(
293
347
  "DELETE",
294
348
  buildUrl(options?.path),
295
- body || null,
349
+ serializedBody,
296
350
  {
297
- headers: options?.headers,
351
+ headers,
298
352
  maxRedirects: options?.maxRedirects,
299
353
  responseType: options?.responseType,
300
354
  }
@@ -523,27 +577,74 @@ export class JirenClient<
523
577
  headersBuffer = Buffer.from(headerStr + "\0");
524
578
  }
525
579
 
526
- const respPtr = lib.symbols.zclient_request(
527
- this.ptr,
528
- methodBuffer,
529
- urlBuffer,
530
- headersBuffer,
531
- bodyBuffer,
532
- maxRedirects,
533
- antibot
534
- );
580
+ // Determine retry configuration
581
+ let retryConfig = this.globalRetry;
582
+ // Check if options is RequestOptions (has 'retry' property and is not just a header map)
583
+ // We already normalized this earlier, but let's be safe.
584
+ // If it has 'responseType', 'method', etc., it's RequestOptions.
585
+ // Simpler: Just check if 'retry' is number or object.
586
+ if (options && typeof options === "object" && "retry" in options) {
587
+ const userRetry = (options as any).retry;
588
+ if (typeof userRetry === "number") {
589
+ retryConfig = { count: userRetry, delay: 100, backoff: 2 };
590
+ } else if (typeof userRetry === "object") {
591
+ retryConfig = userRetry;
592
+ }
593
+ }
535
594
 
536
- const response = this.parseResponse<T>(respPtr, url);
595
+ let attempts = 0;
596
+ // Default to 1 attempt (0 retries) if no config
597
+ const maxAttempts = (retryConfig?.count || 0) + 1;
598
+ let currentDelay = retryConfig?.delay || 100;
599
+ const backoff = retryConfig?.backoff || 2;
600
+
601
+ let lastError: any;
602
+
603
+ while (attempts < maxAttempts) {
604
+ attempts++;
605
+ try {
606
+ const respPtr = lib.symbols.zclient_request(
607
+ this.ptr,
608
+ methodBuffer,
609
+ urlBuffer,
610
+ headersBuffer,
611
+ bodyBuffer,
612
+ maxRedirects,
613
+ antibot
614
+ );
615
+
616
+ if (!respPtr) {
617
+ throw new Error("Native request failed (returned null pointer)");
618
+ }
537
619
 
538
- // Auto-parse if requested
539
- if (responseType) {
540
- if (responseType === "json") return response.body.json();
541
- if (responseType === "text") return response.body.text();
542
- if (responseType === "arraybuffer") return response.body.arrayBuffer();
543
- if (responseType === "blob") return response.body.blob();
620
+ const response = this.parseResponse<T>(respPtr, url);
621
+
622
+ // Optional: Retry on specific status codes (e.g., 500, 502, 503, 504)
623
+ // For now, we only retry on actual exceptions/network failures (null ptr)
624
+ // or if we decide to throw on 5xx here.
625
+ // Let's stick to "Network Failure" retries for now as per plan.
626
+
627
+ // Auto-parse if requested
628
+ if (responseType) {
629
+ if (responseType === "json") return response.body.json();
630
+ if (responseType === "text") return response.body.text();
631
+ if (responseType === "arraybuffer")
632
+ return response.body.arrayBuffer();
633
+ if (responseType === "blob") return response.body.blob();
634
+ }
635
+
636
+ return response;
637
+ } catch (err) {
638
+ lastError = err;
639
+ if (attempts < maxAttempts) {
640
+ // Wait before retrying
641
+ await this.waitFor(currentDelay);
642
+ currentDelay *= backoff;
643
+ }
644
+ }
544
645
  }
545
646
 
546
- return response;
647
+ throw lastError || new Error("Request failed after retries");
547
648
  }
548
649
 
549
650
  private parseResponse<T = any>(
@@ -652,6 +753,35 @@ export class JirenClient<
652
753
  lib.symbols.zclient_response_free(respPtr);
653
754
  }
654
755
  }
756
+
757
+ /**
758
+ * Helper to prepare body and headers for requests.
759
+ * Handles JSON stringification and Content-Type header.
760
+ */
761
+ private prepareBody(
762
+ body: string | object | null | undefined,
763
+ userHeaders?: Record<string, string>
764
+ ): { headers: Record<string, string>; serializedBody: string | null } {
765
+ let serializedBody: string | null = null;
766
+ const headers = { ...userHeaders };
767
+
768
+ if (body !== null && body !== undefined) {
769
+ if (typeof body === "object") {
770
+ serializedBody = JSON.stringify(body);
771
+ // Add Content-Type if not present (case-insensitive check)
772
+ const hasContentType = Object.keys(headers).some(
773
+ (k) => k.toLowerCase() === "content-type"
774
+ );
775
+ if (!hasContentType) {
776
+ headers["Content-Type"] = "application/json";
777
+ }
778
+ } else {
779
+ serializedBody = String(body);
780
+ }
781
+ }
782
+
783
+ return { headers, serializedBody };
784
+ }
655
785
  }
656
786
 
657
787
  class NativeHeaders {
@@ -108,18 +108,32 @@ export interface CacheConfig {
108
108
  maxSize?: number;
109
109
  }
110
110
 
111
+ /** Configuration for automatic request retries */
112
+ export interface RetryConfig {
113
+ /** Number of retry attempts (default: 0) */
114
+ count?: number;
115
+ /** Initial delay in milliseconds before first retry (default: 100) */
116
+ delay?: number;
117
+ /** Backoff multiplier for subsequent retries (2 = double delay each time) (default: 2) */
118
+ backoff?: number;
119
+ }
120
+
111
121
  /** Options for URL endpoint requests */
112
122
  export interface UrlRequestOptions {
113
123
  /** Request headers */
114
124
  headers?: Record<string, string>;
115
125
  /** Path to append to the base URL */
116
126
  path?: string;
127
+ /** Request body (for POST, PUT, PATCH, DELETE) - Auto-stringifies objects */
128
+ body?: string | object | null;
117
129
  /** Maximum number of redirects to follow */
118
130
  maxRedirects?: number;
119
131
  /** Automatically parse response body */
120
132
  responseType?: "json" | "text" | "arraybuffer" | "blob";
121
133
  /** Enable anti-bot protection (using curl-impersonate) */
122
134
  antibot?: boolean;
135
+ /** Retry configuration for this request */
136
+ retry?: number | RetryConfig;
123
137
  }
124
138
 
125
139
  /** Interface for a URL endpoint with HTTP method helpers */
@@ -134,42 +148,26 @@ export interface UrlEndpoint {
134
148
  ): Promise<string>;
135
149
 
136
150
  /** POST request */
151
+ post<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
137
152
  post<T = any>(
138
- body?: string | null,
139
- options?: UrlRequestOptions
140
- ): Promise<JirenResponse<T>>;
141
- post<T = any>(
142
- body?: string | null,
143
153
  options?: UrlRequestOptions & { responseType: "json" }
144
154
  ): Promise<T>;
145
155
 
146
156
  /** PUT request */
157
+ put<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
147
158
  put<T = any>(
148
- body?: string | null,
149
- options?: UrlRequestOptions
150
- ): Promise<JirenResponse<T>>;
151
- put<T = any>(
152
- body?: string | null,
153
159
  options?: UrlRequestOptions & { responseType: "json" }
154
160
  ): Promise<T>;
155
161
 
156
162
  /** PATCH request */
163
+ patch<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
157
164
  patch<T = any>(
158
- body?: string | null,
159
- options?: UrlRequestOptions
160
- ): Promise<JirenResponse<T>>;
161
- patch<T = any>(
162
- body?: string | null,
163
165
  options?: UrlRequestOptions & { responseType: "json" }
164
166
  ): Promise<T>;
165
167
 
166
168
  /** DELETE request */
169
+ delete<T = any>(options?: UrlRequestOptions): Promise<JirenResponse<T>>;
167
170
  delete<T = any>(
168
- body?: string | null,
169
- options?: UrlRequestOptions
170
- ): Promise<JirenResponse<T>>;
171
- delete<T = any>(
172
- body?: string | null,
173
171
  options?: UrlRequestOptions & { responseType: "json" }
174
172
  ): Promise<T>;
175
173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "author": "",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",