jiren 1.1.5 → 1.2.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 +428 -127
- package/components/cache.ts +181 -0
- package/components/client.ts +120 -24
- package/components/types.ts +13 -0
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
# Jiren
|
|
1
|
+
# Jiren 🚀
|
|
2
2
|
|
|
3
|
-
Ultra-fast HTTP
|
|
4
|
-
Designed to be significantly faster than `fetch` and other Node/Bun HTTP clients.
|
|
3
|
+
**Ultra-fast HTTP client powered by native Zig** - Significantly faster than `fetch` and other HTTP clients.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/jiren)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
8
|
+
## Why Jiren?
|
|
9
|
+
|
|
10
|
+
- ⚡ **2-3x faster** than Bun's native `fetch`
|
|
11
|
+
- 🔒 **Anti-bot protection** - Bypass Cloudflare with Chrome TLS fingerprinting
|
|
12
|
+
- 🚄 **HTTP/3 (QUIC)** support with automatic fallback to HTTP/2
|
|
13
|
+
- 💾 **Smart caching** - Persistent gzip-compressed response cache
|
|
14
|
+
- 🔌 **Connection pooling** - Reuse connections for maximum performance
|
|
15
|
+
- 📝 **Type-safe** - Full TypeScript support with autocomplete
|
|
16
|
+
- 🎯 **Zero dependencies** - Pure Zig native module
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -20,223 +21,523 @@ Designed to be significantly faster than `fetch` and other Node/Bun HTTP clients
|
|
|
20
21
|
bun add jiren
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
### Basic Usage (Type-Safe URLs)
|
|
26
|
-
|
|
27
|
-
**Warmup is now mandatory** - you must define URLs in the constructor:
|
|
24
|
+
## Quick Start
|
|
28
25
|
|
|
29
26
|
```typescript
|
|
30
27
|
import { JirenClient } from "jiren";
|
|
31
28
|
|
|
29
|
+
// Create client with warmup URLs
|
|
32
30
|
const client = new JirenClient({
|
|
33
31
|
warmup: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
myapi: "https://api.myservice.com",
|
|
32
|
+
api: "https://api.example.com",
|
|
33
|
+
cdn: "https://cdn.example.com",
|
|
37
34
|
},
|
|
38
35
|
});
|
|
39
36
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
// Make requests with full type safety
|
|
38
|
+
const response = await client.url.api.get({ path: "/users" });
|
|
39
|
+
const users = await response.body.json();
|
|
40
|
+
|
|
41
|
+
console.log(users);
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Basic Usage](#basic-usage)
|
|
47
|
+
- [Response Caching](#response-caching)
|
|
48
|
+
- [Anti-Bot Protection](#anti-bot-protection)
|
|
49
|
+
- [Advanced Features](#advanced-features)
|
|
50
|
+
- [Performance](#performance)
|
|
51
|
+
- [API Reference](#api-reference)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Basic Usage
|
|
56
|
+
|
|
57
|
+
### 1. Create a Client
|
|
58
|
+
|
|
59
|
+
**Warmup is mandatory** - define your URLs upfront for optimal performance:
|
|
47
60
|
|
|
48
61
|
```typescript
|
|
49
|
-
|
|
50
|
-
id: number;
|
|
51
|
-
name: string;
|
|
52
|
-
}
|
|
62
|
+
import { JirenClient } from "jiren";
|
|
53
63
|
|
|
54
64
|
const client = new JirenClient({
|
|
55
65
|
warmup: {
|
|
56
|
-
|
|
66
|
+
github: "https://api.github.com",
|
|
67
|
+
google: "https://www.google.com",
|
|
57
68
|
},
|
|
58
69
|
});
|
|
70
|
+
```
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
### 2. Make GET Requests
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Simple GET request
|
|
76
|
+
const response = await client.url.github.get();
|
|
77
|
+
console.log(response.status); // 200
|
|
78
|
+
|
|
79
|
+
// GET with path and headers
|
|
80
|
+
const user = await client.url.github.get({
|
|
81
|
+
path: "/users/octocat",
|
|
82
|
+
headers: { Authorization: "token YOUR_TOKEN" },
|
|
63
83
|
});
|
|
64
|
-
console.log(res.name); // Fully typed!
|
|
65
84
|
```
|
|
66
85
|
|
|
67
|
-
###
|
|
86
|
+
### 3. Parse Responses
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const response = await client.url.api.get({ path: "/data" });
|
|
90
|
+
|
|
91
|
+
// As JSON
|
|
92
|
+
const json = await response.body.json();
|
|
93
|
+
|
|
94
|
+
// As text
|
|
95
|
+
const text = await response.body.text();
|
|
96
|
+
|
|
97
|
+
// As ArrayBuffer
|
|
98
|
+
const buffer = await response.body.arrayBuffer();
|
|
99
|
+
```
|
|
68
100
|
|
|
69
|
-
|
|
101
|
+
### 4. Auto-Parse with `responseType`
|
|
70
102
|
|
|
71
103
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
// Automatically parse JSON
|
|
105
|
+
const users = await client.url.api.get({
|
|
106
|
+
path: "/users",
|
|
107
|
+
responseType: "json", // Returns parsed JSON directly
|
|
76
108
|
});
|
|
77
109
|
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
110
|
+
// Automatically get text
|
|
111
|
+
const html = await client.url.google.get({
|
|
112
|
+
responseType: "text", // Returns string directly
|
|
81
113
|
});
|
|
82
114
|
```
|
|
83
115
|
|
|
84
|
-
### POST, PUT, PATCH, DELETE
|
|
116
|
+
### 5. POST, PUT, PATCH, DELETE
|
|
85
117
|
|
|
86
118
|
```typescript
|
|
87
|
-
|
|
88
|
-
warmup: {
|
|
89
|
-
api: "https://api.myservice.com",
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// POST with body
|
|
119
|
+
// POST with JSON body
|
|
94
120
|
const created = await client.url.api.post(
|
|
95
|
-
JSON.stringify({ name: "
|
|
121
|
+
JSON.stringify({ name: "John Doe", email: "john@example.com" }),
|
|
96
122
|
{
|
|
97
|
-
path: "/
|
|
123
|
+
path: "/users",
|
|
98
124
|
headers: { "Content-Type": "application/json" },
|
|
125
|
+
responseType: "json",
|
|
99
126
|
}
|
|
100
127
|
);
|
|
101
128
|
|
|
102
|
-
// PUT
|
|
103
|
-
await client.url.api.put(
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
// PUT request
|
|
130
|
+
await client.url.api.put(JSON.stringify({ name: "Jane Doe" }), {
|
|
131
|
+
path: "/users/123",
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// PATCH request
|
|
136
|
+
await client.url.api.patch(JSON.stringify({ email: "new@example.com" }), {
|
|
137
|
+
path: "/users/123",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// DELETE request
|
|
141
|
+
await client.url.api.delete(null, { path: "/users/123" });
|
|
106
142
|
```
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
---
|
|
109
145
|
|
|
110
|
-
|
|
111
|
-
const res = await client.url.api.get({ path: "/data" });
|
|
146
|
+
## Response Caching
|
|
112
147
|
|
|
113
|
-
|
|
114
|
-
const text = await res.body.text();
|
|
148
|
+
Enable persistent file-based caching for **instant responses** on subsequent requests:
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
const json = await res.body.json();
|
|
150
|
+
### Basic Caching
|
|
118
151
|
|
|
119
|
-
|
|
120
|
-
const
|
|
152
|
+
```typescript
|
|
153
|
+
const client = new JirenClient({
|
|
154
|
+
warmup: {
|
|
155
|
+
api: {
|
|
156
|
+
url: "https://api.example.com",
|
|
157
|
+
cache: true, // Enable 60-second cache
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
121
161
|
|
|
122
|
-
//
|
|
123
|
-
const
|
|
162
|
+
// First request: Real HTTP request (~150ms)
|
|
163
|
+
const data1 = await client.url.api.get({ path: "/users" });
|
|
124
164
|
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
// Second request: From cache (~1-2ms) - 100x faster! ⚡
|
|
166
|
+
const data2 = await client.url.api.get({ path: "/users" });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Custom Cache TTL
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const client = new JirenClient({
|
|
173
|
+
warmup: {
|
|
174
|
+
api: {
|
|
175
|
+
url: "https://api.example.com",
|
|
176
|
+
cache: { ttl: 300000 }, // 5-minute cache
|
|
177
|
+
},
|
|
178
|
+
cdn: {
|
|
179
|
+
url: "https://cdn.example.com",
|
|
180
|
+
cache: { ttl: 3600000 }, // 1-hour cache
|
|
181
|
+
},
|
|
182
|
+
},
|
|
129
183
|
});
|
|
130
184
|
```
|
|
131
185
|
|
|
132
|
-
|
|
186
|
+
### Manual Cache Refresh
|
|
133
187
|
|
|
134
|
-
|
|
188
|
+
```typescript
|
|
189
|
+
// Refresh cache for a specific endpoint
|
|
190
|
+
await client.url.api.prefetch({ path: "/users" });
|
|
135
191
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
| Mandatory warmup | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
140
|
-
| HTTP/3 (QUIC) | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
141
|
-
| Anti-bot protection | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
142
|
-
| Native performance | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
143
|
-
| Zero code generation | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
144
|
-
| Bun FFI optimized | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
192
|
+
// Now the next request will use fresh data
|
|
193
|
+
const users = await client.url.api.get({ path: "/users" });
|
|
194
|
+
```
|
|
145
195
|
|
|
146
|
-
###
|
|
196
|
+
### Cache Features
|
|
147
197
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
5. **⚡ HTTP/3 Support** - First-class QUIC support for modern protocols
|
|
198
|
+
- ✅ **Persistent** - Survives process restarts
|
|
199
|
+
- ✅ **Compressed** - Gzip-compressed JSON files (`.cache/jiren/*.json.gz`)
|
|
200
|
+
- ✅ **Automatic expiration** - Respects TTL
|
|
201
|
+
- ✅ **Fast** - ~100x faster than real requests
|
|
153
202
|
|
|
154
|
-
|
|
203
|
+
### Cache Performance
|
|
155
204
|
|
|
156
|
-
|
|
205
|
+
| Type | Speed | Use Case |
|
|
206
|
+
| ------------ | ------ | ----------------------------------------------- |
|
|
207
|
+
| Real request | ~150ms | First request or cache miss |
|
|
208
|
+
| File cache | ~1-2ms | Subsequent requests (same or different process) |
|
|
209
|
+
| Memory cache | ~0.1ms | Multiple requests in same process |
|
|
157
210
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
211
|
+
**💡 Tip:** Add `.cache/` to your `.gitignore`:
|
|
212
|
+
|
|
213
|
+
```gitignore
|
|
214
|
+
# Jiren cache
|
|
215
|
+
.cache/
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Anti-Bot Protection
|
|
221
|
+
|
|
222
|
+
Bypass Cloudflare and other bot protection using Chrome TLS fingerprinting:
|
|
162
223
|
|
|
163
224
|
```typescript
|
|
164
|
-
const
|
|
165
|
-
|
|
225
|
+
const client = new JirenClient({
|
|
226
|
+
warmup: {
|
|
227
|
+
protected: "https://cloudflare-protected-site.com",
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Enable anti-bot mode for this request
|
|
232
|
+
const response = await client.url.protected.get({
|
|
233
|
+
antibot: true, // Uses curl-impersonate with Chrome 120 fingerprint
|
|
166
234
|
});
|
|
167
235
|
```
|
|
168
236
|
|
|
169
|
-
**
|
|
237
|
+
**How it works:**
|
|
170
238
|
|
|
171
|
-
|
|
239
|
+
- Uses `curl-impersonate` to mimic Chrome 120 browser
|
|
240
|
+
- Includes realistic TLS fingerprint and headers
|
|
241
|
+
- Bypasses most bot detection systems
|
|
172
242
|
|
|
173
|
-
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Advanced Features
|
|
246
|
+
|
|
247
|
+
### TypeScript Generics
|
|
174
248
|
|
|
175
249
|
```typescript
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
250
|
+
interface User {
|
|
251
|
+
id: number;
|
|
252
|
+
name: string;
|
|
253
|
+
email: string;
|
|
179
254
|
}
|
|
180
255
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
256
|
+
// Type-safe response
|
|
257
|
+
const user = await client.url.api.get<User>({
|
|
258
|
+
path: "/users/123",
|
|
259
|
+
responseType: "json",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
console.log(user.name); // TypeScript knows this is a string
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Multiple Warmup Formats
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Object format (recommended)
|
|
269
|
+
const client1 = new JirenClient({
|
|
183
270
|
warmup: {
|
|
184
271
|
api: "https://api.example.com",
|
|
185
272
|
cdn: "https://cdn.example.com",
|
|
186
273
|
},
|
|
187
274
|
});
|
|
188
275
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
276
|
+
// Array format (with cache config)
|
|
277
|
+
const client2 = new JirenClient({
|
|
278
|
+
warmup: [
|
|
279
|
+
{ key: "api", url: "https://api.example.com", cache: true },
|
|
280
|
+
{ key: "cdn", url: "https://cdn.example.com", cache: { ttl: 3600000 } },
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Simple array
|
|
285
|
+
const client3 = new JirenClient({
|
|
286
|
+
warmup: ["https://api.example.com", "https://cdn.example.com"],
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Benchmark Mode
|
|
197
291
|
|
|
198
|
-
|
|
199
|
-
|
|
292
|
+
Force HTTP/2 for consistent benchmarking:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
const client = new JirenClient({
|
|
296
|
+
warmup: { api: "https://api.example.com" },
|
|
297
|
+
benchmark: true, // Disables HTTP/3 probing, forces HTTP/2
|
|
298
|
+
});
|
|
200
299
|
```
|
|
201
300
|
|
|
301
|
+
### Close Client
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// Clean up resources when done
|
|
305
|
+
client.close();
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Performance
|
|
311
|
+
|
|
312
|
+
### Benchmark Results
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
Bun fetch: ~300ms
|
|
316
|
+
JirenClient: ~120ms (2.5x faster)
|
|
317
|
+
JirenClient (cached): ~1-2ms (150x faster)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Performance Tips
|
|
321
|
+
|
|
322
|
+
1. **Always use warmup** - Pre-establishes connections
|
|
323
|
+
2. **Enable caching** - For data that doesn't change frequently
|
|
324
|
+
3. **Reuse client instances** - Don't create new clients for each request
|
|
325
|
+
4. **Use `responseType`** - Automatic parsing is faster
|
|
326
|
+
5. **Batch requests** - Use `Promise.all()` for concurrent requests
|
|
327
|
+
|
|
328
|
+
### Why So Fast?
|
|
329
|
+
|
|
330
|
+
- **Native Zig implementation** - No JavaScript overhead
|
|
331
|
+
- **HTTP/3 (QUIC) support** - Faster than HTTP/2 when available
|
|
332
|
+
- **Connection pooling** - Reuses TCP/TLS connections
|
|
333
|
+
- **Smart caching** - Compressed persistent cache
|
|
334
|
+
- **Zero-copy operations** - Minimal memory allocations
|
|
335
|
+
- **TCP keep-alive** - Prevents connection drops
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## API Reference
|
|
340
|
+
|
|
341
|
+
### `JirenClient` Constructor
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
new JirenClient(options?: JirenClientOptions)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Options:**
|
|
348
|
+
|
|
349
|
+
| Option | Type | Description |
|
|
350
|
+
| ----------- | -------------------------------------------------- | ---------------------------- |
|
|
351
|
+
| `warmup` | `Record<string, UrlConfig>` \| `WarmupUrlConfig[]` | URLs to pre-warm (required) |
|
|
352
|
+
| `benchmark` | `boolean` | Force HTTP/2 mode (optional) |
|
|
353
|
+
|
|
354
|
+
**UrlConfig:**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
type UrlConfig =
|
|
358
|
+
| string
|
|
359
|
+
| {
|
|
360
|
+
url: string;
|
|
361
|
+
cache?: boolean | { ttl: number };
|
|
362
|
+
};
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### URL Endpoint Methods
|
|
366
|
+
|
|
367
|
+
All methods are available on `client.url.<key>`:
|
|
368
|
+
|
|
369
|
+
#### `get(options?)`
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
get<T>(options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### `post(body?, options?)`
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
post<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### `put(body?, options?)`
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
put<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `patch(body?, options?)`
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
patch<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### `delete(body?, options?)`
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
delete<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### `head(options?)`
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
head(options?: UrlRequestOptions): Promise<JirenResponse<any>>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### `options(options?)`
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
options(options?: UrlRequestOptions): Promise<JirenResponse<any>>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### `prefetch(options?)`
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
prefetch(options?: UrlRequestOptions): Promise<void>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Clears cache and makes a fresh request to populate cache.
|
|
418
|
+
|
|
202
419
|
### Request Options
|
|
203
420
|
|
|
204
421
|
```typescript
|
|
205
422
|
interface UrlRequestOptions {
|
|
206
423
|
path?: string; // Path to append to base URL
|
|
207
|
-
headers?: Record<string, string>;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
424
|
+
headers?: Record<string, string>; // Request headers
|
|
425
|
+
responseType?: "json" | "text"; // Auto-parse response
|
|
426
|
+
antibot?: boolean; // Enable anti-bot protection
|
|
427
|
+
maxRedirects?: number; // Max redirects to follow
|
|
211
428
|
}
|
|
212
429
|
```
|
|
213
430
|
|
|
214
431
|
### Response Object
|
|
215
432
|
|
|
216
433
|
```typescript
|
|
217
|
-
interface JirenResponse<T> {
|
|
434
|
+
interface JirenResponse<T = any> {
|
|
435
|
+
url: string;
|
|
218
436
|
status: number;
|
|
219
437
|
statusText: string;
|
|
220
438
|
headers: Record<string, string>;
|
|
221
439
|
ok: boolean;
|
|
440
|
+
redirected: boolean;
|
|
441
|
+
type: string;
|
|
222
442
|
body: {
|
|
223
|
-
text(): Promise<string>;
|
|
224
443
|
json<R = T>(): Promise<R>;
|
|
444
|
+
text(): Promise<string>;
|
|
225
445
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
226
446
|
blob(): Promise<Blob>;
|
|
227
447
|
};
|
|
228
448
|
}
|
|
229
449
|
```
|
|
230
450
|
|
|
231
|
-
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Examples
|
|
454
|
+
|
|
455
|
+
### Real-World React App
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// api-client.ts
|
|
459
|
+
import { JirenClient } from "jiren";
|
|
460
|
+
|
|
461
|
+
export const apiClient = new JirenClient({
|
|
462
|
+
warmup: {
|
|
463
|
+
backend: {
|
|
464
|
+
url: "https://api.myapp.com",
|
|
465
|
+
cache: true, // Cache API responses
|
|
466
|
+
},
|
|
467
|
+
cdn: {
|
|
468
|
+
url: "https://cdn.myapp.com",
|
|
469
|
+
cache: { ttl: 3600000 }, // 1-hour cache for static assets
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
});
|
|
232
473
|
|
|
233
|
-
|
|
474
|
+
// UserProfile.tsx
|
|
475
|
+
import { apiClient } from "./api-client";
|
|
234
476
|
|
|
235
|
-
|
|
236
|
-
|
|
477
|
+
function UserProfile() {
|
|
478
|
+
const [user, setUser] = useState(null);
|
|
237
479
|
|
|
238
|
-
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
// Fast! Uses cache if available
|
|
482
|
+
apiClient.url.backend
|
|
483
|
+
.get({ path: "/user/me", responseType: "json" })
|
|
484
|
+
.then(setUser);
|
|
485
|
+
}, []);
|
|
486
|
+
|
|
487
|
+
return <div>{user?.name}</div>;
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Polling with Cache
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
const client = new JirenClient({
|
|
495
|
+
warmup: {
|
|
496
|
+
api: {
|
|
497
|
+
url: "https://api.example.com",
|
|
498
|
+
cache: { ttl: 30000 }, // 30-second cache
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Poll every 10 seconds
|
|
504
|
+
setInterval(async () => {
|
|
505
|
+
// First 3 requests use cache, 4th request is fresh
|
|
506
|
+
const notifications = await client.url.api.get({
|
|
507
|
+
path: "/notifications",
|
|
508
|
+
responseType: "json",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
updateUI(notifications);
|
|
512
|
+
}, 10000);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Requirements
|
|
518
|
+
|
|
519
|
+
- **Bun** v1.0.0 or higher
|
|
520
|
+
|
|
521
|
+
---
|
|
239
522
|
|
|
240
523
|
## License
|
|
241
524
|
|
|
242
|
-
MIT
|
|
525
|
+
MIT © Vikash Khati
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Contributing
|
|
530
|
+
|
|
531
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Support
|
|
536
|
+
|
|
537
|
+
- 📖 [Documentation](https://github.com/vikashkhati007/jiren)
|
|
538
|
+
- 🐛 [Issue Tracker](https://github.com/vikashkhati007/jiren/issues)
|
|
539
|
+
- 💬 [Discussions](https://github.com/vikashkhati007/jiren/discussions)
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
**Made with ⚡ by the Jiren team**
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import type { JirenResponse } from "./types";
|
|
6
|
+
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
response: JirenResponse;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
ttl: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ResponseCache {
|
|
14
|
+
private cacheDir: string;
|
|
15
|
+
private maxSize: number;
|
|
16
|
+
|
|
17
|
+
constructor(maxSize = 100, cacheDir = ".cache/jiren") {
|
|
18
|
+
this.maxSize = maxSize;
|
|
19
|
+
this.cacheDir = cacheDir;
|
|
20
|
+
|
|
21
|
+
// Create cache directory if it doesn't exist
|
|
22
|
+
if (!existsSync(this.cacheDir)) {
|
|
23
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate cache key from URL and options
|
|
29
|
+
*/
|
|
30
|
+
private generateKey(url: string, path?: string, options?: any): string {
|
|
31
|
+
const fullUrl = path ? `${url}${path}` : url;
|
|
32
|
+
const method = options?.method || "GET";
|
|
33
|
+
const headers = JSON.stringify(options?.headers || {});
|
|
34
|
+
const key = `${method}:${fullUrl}:${headers}`;
|
|
35
|
+
|
|
36
|
+
// Hash the key to create a valid filename
|
|
37
|
+
return createHash("md5").update(key).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get cache file path (compressed .gz file)
|
|
42
|
+
*/
|
|
43
|
+
private getCacheFilePath(key: string): string {
|
|
44
|
+
return join(this.cacheDir, `${key}.json.gz`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get cached response if valid
|
|
49
|
+
*/
|
|
50
|
+
get(url: string, path?: string, options?: any): JirenResponse | null {
|
|
51
|
+
const key = this.generateKey(url, path, options);
|
|
52
|
+
const filePath = this.getCacheFilePath(key);
|
|
53
|
+
|
|
54
|
+
if (!existsSync(filePath)) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Read compressed file
|
|
58
|
+
const compressed = readFileSync(filePath);
|
|
59
|
+
|
|
60
|
+
// Decompress
|
|
61
|
+
const decompressed = gunzipSync(compressed);
|
|
62
|
+
const data = decompressed.toString("utf-8");
|
|
63
|
+
const entry: CacheEntry = JSON.parse(data);
|
|
64
|
+
|
|
65
|
+
// Check if expired
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
68
|
+
// Delete expired cache file
|
|
69
|
+
try {
|
|
70
|
+
require("fs").unlinkSync(filePath);
|
|
71
|
+
} catch {}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return entry.response;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Invalid cache file, delete it
|
|
78
|
+
try {
|
|
79
|
+
require("fs").unlinkSync(filePath);
|
|
80
|
+
} catch {}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Store response in cache as compressed JSON file
|
|
87
|
+
*/
|
|
88
|
+
set(
|
|
89
|
+
url: string,
|
|
90
|
+
response: JirenResponse,
|
|
91
|
+
ttl: number,
|
|
92
|
+
path?: string,
|
|
93
|
+
options?: any
|
|
94
|
+
): void {
|
|
95
|
+
const key = this.generateKey(url, path, options);
|
|
96
|
+
const filePath = this.getCacheFilePath(key);
|
|
97
|
+
|
|
98
|
+
const entry: CacheEntry = {
|
|
99
|
+
response,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
ttl,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Convert to JSON
|
|
106
|
+
const json = JSON.stringify(entry);
|
|
107
|
+
|
|
108
|
+
// Compress with gzip
|
|
109
|
+
const compressed = gzipSync(json);
|
|
110
|
+
|
|
111
|
+
// Write compressed file
|
|
112
|
+
writeFileSync(filePath, compressed);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// Silently fail if can't write cache
|
|
115
|
+
console.warn("Failed to write cache:", error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clear cache for a specific URL or all
|
|
121
|
+
*/
|
|
122
|
+
clear(url?: string): void {
|
|
123
|
+
if (url) {
|
|
124
|
+
// Clear all cache files for this URL
|
|
125
|
+
// This is approximate since we hash the keys
|
|
126
|
+
// For now, just clear all to be safe
|
|
127
|
+
this.clearAll();
|
|
128
|
+
} else {
|
|
129
|
+
this.clearAll();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clear all cache files
|
|
135
|
+
*/
|
|
136
|
+
private clearAll(): void {
|
|
137
|
+
try {
|
|
138
|
+
const fs = require("fs");
|
|
139
|
+
const files = fs.readdirSync(this.cacheDir);
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
if (file.endsWith(".json.gz")) {
|
|
142
|
+
fs.unlinkSync(join(this.cacheDir, file));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Silently fail
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get cache statistics
|
|
152
|
+
*/
|
|
153
|
+
stats() {
|
|
154
|
+
try {
|
|
155
|
+
const fs = require("fs");
|
|
156
|
+
const files = fs.readdirSync(this.cacheDir);
|
|
157
|
+
const cacheFiles = files.filter((f: string) => f.endsWith(".json.gz"));
|
|
158
|
+
|
|
159
|
+
// Calculate total size
|
|
160
|
+
let totalSize = 0;
|
|
161
|
+
for (const file of cacheFiles) {
|
|
162
|
+
const stats = fs.statSync(join(this.cacheDir, file));
|
|
163
|
+
totalSize += stats.size;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
size: cacheFiles.length,
|
|
168
|
+
maxSize: this.maxSize,
|
|
169
|
+
cacheDir: this.cacheDir,
|
|
170
|
+
totalSizeKB: (totalSize / 1024).toFixed(2),
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
return {
|
|
174
|
+
size: 0,
|
|
175
|
+
maxSize: this.maxSize,
|
|
176
|
+
cacheDir: this.cacheDir,
|
|
177
|
+
totalSizeKB: "0",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
package/components/client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CString, toArrayBuffer, type Pointer } from "bun:ffi";
|
|
2
2
|
import { lib } from "./native";
|
|
3
|
+
import { ResponseCache } from "./cache";
|
|
3
4
|
import type {
|
|
4
5
|
RequestOptions,
|
|
5
6
|
JirenResponse,
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
WarmupUrlConfig,
|
|
8
9
|
UrlRequestOptions,
|
|
9
10
|
UrlEndpoint,
|
|
11
|
+
CacheConfig,
|
|
10
12
|
} from "./types";
|
|
11
13
|
|
|
12
14
|
const STATUS_TEXT: Record<number, string> = {
|
|
@@ -24,11 +26,14 @@ const STATUS_TEXT: Record<number, string> = {
|
|
|
24
26
|
503: "Service Unavailable",
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
/** URL configuration with optional cache */
|
|
30
|
+
export type UrlConfig = string | { url: string; cache?: boolean | CacheConfig };
|
|
31
|
+
|
|
27
32
|
/** Options for JirenClient constructor */
|
|
28
33
|
export interface JirenClientOptions<
|
|
29
|
-
T extends readonly WarmupUrlConfig[] | Record<string,
|
|
34
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
30
35
|
| readonly WarmupUrlConfig[]
|
|
31
|
-
| Record<string,
|
|
36
|
+
| Record<string, UrlConfig>
|
|
32
37
|
> {
|
|
33
38
|
/** URLs to warmup on client creation (pre-connect + handshake) */
|
|
34
39
|
warmup?: string[] | T;
|
|
@@ -39,16 +44,16 @@ export interface JirenClientOptions<
|
|
|
39
44
|
|
|
40
45
|
/** Helper to extract keys from Warmup Config */
|
|
41
46
|
export type ExtractWarmupKeys<
|
|
42
|
-
T extends readonly WarmupUrlConfig[] | Record<string,
|
|
47
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
43
48
|
> = T extends readonly WarmupUrlConfig[]
|
|
44
49
|
? T[number]["key"]
|
|
45
|
-
: T extends Record<string,
|
|
50
|
+
: T extends Record<string, UrlConfig>
|
|
46
51
|
? keyof T
|
|
47
52
|
: never;
|
|
48
53
|
|
|
49
54
|
/** Type-safe URL accessor - maps keys to UrlEndpoint objects */
|
|
50
55
|
export type UrlAccessor<
|
|
51
|
-
T extends readonly WarmupUrlConfig[] | Record<string,
|
|
56
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig>
|
|
52
57
|
> = {
|
|
53
58
|
[K in ExtractWarmupKeys<T>]: UrlEndpoint;
|
|
54
59
|
};
|
|
@@ -79,12 +84,15 @@ export function defineUrls<const T extends readonly WarmupUrlConfig[]>(
|
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export class JirenClient<
|
|
82
|
-
T extends readonly WarmupUrlConfig[] | Record<string,
|
|
87
|
+
T extends readonly WarmupUrlConfig[] | Record<string, UrlConfig> =
|
|
83
88
|
| readonly WarmupUrlConfig[]
|
|
84
|
-
| Record<string,
|
|
89
|
+
| Record<string, UrlConfig>
|
|
85
90
|
> {
|
|
86
91
|
private ptr: Pointer | null;
|
|
87
92
|
private urlMap: Map<string, string> = new Map();
|
|
93
|
+
private cacheConfig: Map<string, { enabled: boolean; ttl: number }> =
|
|
94
|
+
new Map();
|
|
95
|
+
private cache: ResponseCache;
|
|
88
96
|
|
|
89
97
|
/** Type-safe URL accessor for warmed-up URLs */
|
|
90
98
|
public readonly url: UrlAccessor<T>;
|
|
@@ -93,6 +101,9 @@ export class JirenClient<
|
|
|
93
101
|
this.ptr = lib.symbols.zclient_new();
|
|
94
102
|
if (!this.ptr) throw new Error("Failed to create native client instance");
|
|
95
103
|
|
|
104
|
+
// Initialize cache
|
|
105
|
+
this.cache = new ResponseCache(100);
|
|
106
|
+
|
|
96
107
|
// Enable benchmark mode if requested
|
|
97
108
|
if (options?.benchmark) {
|
|
98
109
|
lib.symbols.zclient_set_benchmark_mode(this.ptr, true);
|
|
@@ -108,17 +119,42 @@ export class JirenClient<
|
|
|
108
119
|
if (typeof item === "string") {
|
|
109
120
|
urls.push(item);
|
|
110
121
|
} else {
|
|
111
|
-
// WarmupUrlConfig with key
|
|
122
|
+
// WarmupUrlConfig with key and optional cache
|
|
112
123
|
const config = item as WarmupUrlConfig;
|
|
113
124
|
urls.push(config.url);
|
|
114
125
|
this.urlMap.set(config.key, config.url);
|
|
126
|
+
|
|
127
|
+
// Store cache config
|
|
128
|
+
if (config.cache) {
|
|
129
|
+
const cacheConfig =
|
|
130
|
+
typeof config.cache === "boolean"
|
|
131
|
+
? { enabled: true, ttl: 60000 }
|
|
132
|
+
: { enabled: true, ttl: config.cache.ttl || 60000 };
|
|
133
|
+
this.cacheConfig.set(config.key, cacheConfig);
|
|
134
|
+
}
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
} else {
|
|
118
|
-
// Record<string,
|
|
119
|
-
for (const [key,
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
// Record<string, UrlConfig>
|
|
139
|
+
for (const [key, urlConfig] of Object.entries(warmup)) {
|
|
140
|
+
if (typeof urlConfig === "string") {
|
|
141
|
+
// Simple string URL
|
|
142
|
+
urls.push(urlConfig);
|
|
143
|
+
this.urlMap.set(key, urlConfig);
|
|
144
|
+
} else {
|
|
145
|
+
// URL config object with cache
|
|
146
|
+
urls.push(urlConfig.url);
|
|
147
|
+
this.urlMap.set(key, urlConfig.url);
|
|
148
|
+
|
|
149
|
+
// Store cache config
|
|
150
|
+
if (urlConfig.cache) {
|
|
151
|
+
const cacheConfig =
|
|
152
|
+
typeof urlConfig.cache === "boolean"
|
|
153
|
+
? { enabled: true, ttl: 60000 }
|
|
154
|
+
: { enabled: true, ttl: urlConfig.cache.ttl || 60000 };
|
|
155
|
+
this.cacheConfig.set(key, cacheConfig);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
122
158
|
}
|
|
123
159
|
}
|
|
124
160
|
|
|
@@ -159,12 +195,46 @@ export class JirenClient<
|
|
|
159
195
|
get: async <R = any>(
|
|
160
196
|
options?: UrlRequestOptions
|
|
161
197
|
): Promise<JirenResponse<R> | R | string | ArrayBuffer | Blob> => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
// Check if caching is enabled for this URL
|
|
199
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
200
|
+
|
|
201
|
+
if (cacheConfig?.enabled) {
|
|
202
|
+
// Try to get from cache
|
|
203
|
+
const cached = self.cache.get(baseUrl, options?.path, options);
|
|
204
|
+
if (cached) {
|
|
205
|
+
return cached as any;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
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
|
+
);
|
|
221
|
+
|
|
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
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return response;
|
|
168
238
|
},
|
|
169
239
|
|
|
170
240
|
post: async <R = any>(
|
|
@@ -250,6 +320,25 @@ export class JirenClient<
|
|
|
250
320
|
antibot: options?.antibot,
|
|
251
321
|
});
|
|
252
322
|
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Prefetch/refresh cache for this URL
|
|
326
|
+
* Clears existing cache and makes a fresh request
|
|
327
|
+
*/
|
|
328
|
+
prefetch: async (options?: UrlRequestOptions): Promise<void> => {
|
|
329
|
+
// Clear cache for this URL
|
|
330
|
+
self.cache.clear(baseUrl);
|
|
331
|
+
|
|
332
|
+
// Make fresh request to populate cache
|
|
333
|
+
const cacheConfig = self.cacheConfig.get(prop);
|
|
334
|
+
if (cacheConfig?.enabled) {
|
|
335
|
+
await self.request("GET", buildUrl(options?.path), null, {
|
|
336
|
+
headers: options?.headers,
|
|
337
|
+
maxRedirects: options?.maxRedirects,
|
|
338
|
+
antibot: options?.antibot,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
},
|
|
253
342
|
} as UrlEndpoint;
|
|
254
343
|
},
|
|
255
344
|
});
|
|
@@ -267,17 +356,24 @@ export class JirenClient<
|
|
|
267
356
|
}
|
|
268
357
|
|
|
269
358
|
/**
|
|
270
|
-
* Warm up connections to URLs (DNS resolve + QUIC handshake).
|
|
359
|
+
* Warm up connections to URLs (DNS resolve + QUIC handshake) in parallel.
|
|
271
360
|
* Call this early (e.g., at app startup) so subsequent requests are fast.
|
|
272
361
|
* @param urls - List of URLs to warm up
|
|
273
362
|
*/
|
|
274
|
-
public warmup(urls: string[]): void {
|
|
363
|
+
public async warmup(urls: string[]): Promise<void> {
|
|
275
364
|
if (!this.ptr) throw new Error("Client is closed");
|
|
276
365
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
366
|
+
// Warm up all URLs in parallel for faster startup
|
|
367
|
+
await Promise.all(
|
|
368
|
+
urls.map(
|
|
369
|
+
(url) =>
|
|
370
|
+
new Promise<void>((resolve) => {
|
|
371
|
+
const urlBuffer = Buffer.from(url + "\0");
|
|
372
|
+
lib.symbols.zclient_prefetch(this.ptr, urlBuffer);
|
|
373
|
+
resolve();
|
|
374
|
+
})
|
|
375
|
+
)
|
|
376
|
+
);
|
|
281
377
|
}
|
|
282
378
|
|
|
283
379
|
/**
|
package/components/types.ts
CHANGED
|
@@ -96,6 +96,16 @@ export interface WarmupUrlConfig {
|
|
|
96
96
|
key: string;
|
|
97
97
|
/** The URL to warmup */
|
|
98
98
|
url: string;
|
|
99
|
+
/** Enable response caching for this URL (default: false) */
|
|
100
|
+
cache?: boolean | CacheConfig;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Cache configuration */
|
|
104
|
+
export interface CacheConfig {
|
|
105
|
+
/** Cache TTL in milliseconds (default: 60000 = 1 minute) */
|
|
106
|
+
ttl?: number;
|
|
107
|
+
/** Maximum cache size (default: 100 entries) */
|
|
108
|
+
maxSize?: number;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
/** Options for URL endpoint requests */
|
|
@@ -168,6 +178,9 @@ export interface UrlEndpoint {
|
|
|
168
178
|
|
|
169
179
|
/** OPTIONS request */
|
|
170
180
|
options(options?: UrlRequestOptions): Promise<JirenResponse<any>>;
|
|
181
|
+
|
|
182
|
+
/** Prefetch/refresh cache for this URL */
|
|
183
|
+
prefetch(options?: UrlRequestOptions): Promise<void>;
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
/** Type helper to extract keys from warmup config array */
|
package/lib/libhttpclient.dylib
CHANGED
|
Binary file
|