jiren 1.5.0 → 1.6.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 +321 -483
- package/components/cache.ts +1 -1
- package/components/client-node-native.ts +497 -159
- package/components/client.ts +51 -29
- package/components/metrics.ts +1 -4
- package/components/native-node.ts +7 -3
- package/components/native.ts +29 -0
- package/components/persistent-worker.ts +73 -0
- package/components/subprocess-worker.ts +65 -0
- package/components/worker-pool.ts +169 -0
- package/components/worker.ts +39 -23
- package/dist/components/cache.d.ts +76 -0
- package/dist/components/cache.d.ts.map +1 -0
- package/dist/components/cache.js +439 -0
- package/dist/components/cache.js.map +1 -0
- package/dist/components/client-node-native.d.ts +114 -0
- package/dist/components/client-node-native.d.ts.map +1 -0
- package/dist/components/client-node-native.js +744 -0
- package/dist/components/client-node-native.js.map +1 -0
- package/dist/components/metrics.d.ts +104 -0
- package/dist/components/metrics.d.ts.map +1 -0
- package/dist/components/metrics.js +296 -0
- package/dist/components/metrics.js.map +1 -0
- package/dist/components/native-node.d.ts +60 -0
- package/dist/components/native-node.d.ts.map +1 -0
- package/dist/components/native-node.js +108 -0
- package/dist/components/native-node.js.map +1 -0
- package/dist/components/types.d.ts +250 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +5 -0
- package/dist/components/types.js.map +1 -0
- package/dist/index-node.d.ts +10 -0
- package/dist/index-node.d.ts.map +1 -0
- package/dist/index-node.js +12 -0
- package/dist/index-node.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/index-node.ts +6 -6
- package/index.ts +4 -3
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +13 -5
- package/types/index.ts +0 -68
package/README.md
CHANGED
|
@@ -1,282 +1,314 @@
|
|
|
1
1
|
# Jiren 🚀
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**The fastest HTTP client for JavaScript** - Simple, type-safe, and blazingly fast.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/jiren)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚡ Performance: Fastest in the World
|
|
11
|
+
|
|
12
|
+
Jiren outperforms top clients in every metric—throughput, latency, and stability.
|
|
13
|
+
|
|
14
|
+
| Client | Avg Latency | P99 Latency | Throughput | Relative Speed |
|
|
15
|
+
| --------- | ----------- | ----------- | ------------ | -------------- |
|
|
16
|
+
| **Jiren** | **21.6 µs** | **46.0 µs** | **46,000/s** | **🚀 Fastest** |
|
|
17
|
+
| Bun Fetch | 30.7 µs | 58.5 µs | 32,500/s | 1.4x Slower |
|
|
18
|
+
| Undici | 34.1 µs | 76.5 µs | 29,300/s | 1.6x Slower |
|
|
19
|
+
| Ky | 37.2 µs | 57.0 µs | 26,900/s | 1.7x Slower |
|
|
20
|
+
| Axios | 59.6 µs | 122.1 µs | 16,700/s | 2.8x Slower |
|
|
9
21
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Why Jiren?
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
| Feature | Benefit |
|
|
27
|
+
| ----------------------- | ------------------------------------------------- |
|
|
28
|
+
| ⚡ **Blazing Fast** | Proven to be the fastest JS HTTP client |
|
|
29
|
+
| **HTTP/3 Support** | Automatic protocol upgrade for faster connections |
|
|
30
|
+
| 💾 **Built-in Caching** | Automatic response caching with zero config |
|
|
31
|
+
| 📝 **Type-Safe** | Full TypeScript support with autocomplete |
|
|
32
|
+
| 🔒 **Anti-Bot Ready** | Bypass common bot protections easily |
|
|
33
|
+
| 📊 **Built-in Metrics** | Track performance out of the box |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
19
38
|
|
|
20
39
|
```bash
|
|
21
40
|
bun add jiren
|
|
22
41
|
```
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🚀 Quick Start
|
|
46
|
+
|
|
47
|
+
### Step 1: Create Your Client
|
|
25
48
|
|
|
26
49
|
```typescript
|
|
27
50
|
import { JirenClient } from "jiren";
|
|
28
51
|
|
|
29
|
-
// Create client with warmup URLs
|
|
30
52
|
const client = new JirenClient({
|
|
31
|
-
|
|
53
|
+
targets: {
|
|
32
54
|
api: "https://api.example.com",
|
|
33
|
-
|
|
55
|
+
github: "https://api.github.com",
|
|
34
56
|
},
|
|
35
57
|
});
|
|
58
|
+
```
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
### Step 2: Make Requests
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// GET request
|
|
38
64
|
const response = await client.url.api.get({ path: "/users" });
|
|
39
65
|
const users = await response.body.json();
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
// POST request
|
|
68
|
+
await client.url.api.post(JSON.stringify({ name: "John" }), {
|
|
69
|
+
path: "/users",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
});
|
|
42
72
|
```
|
|
43
73
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- [Basic Usage](#basic-usage)
|
|
47
|
-
- [Response Caching](#response-caching)
|
|
48
|
-
- [Anti-Bot Protection](#anti-bot-protection)
|
|
49
|
-
- [Request Interceptors](#request-interceptors)
|
|
50
|
-
- [Metrics & Observability](#metrics--observability)
|
|
51
|
-
- [Advanced Features](#advanced-features)
|
|
52
|
-
- [Performance](#performance)
|
|
53
|
-
- [API Reference](#api-reference)
|
|
74
|
+
That's it! 🎉
|
|
54
75
|
|
|
55
76
|
---
|
|
56
77
|
|
|
57
|
-
##
|
|
78
|
+
## 📖 Table of Contents
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
1. [Making Requests](#-making-requests)
|
|
81
|
+
2. [Response Handling](#-response-handling)
|
|
82
|
+
3. [Caching](#-caching)
|
|
83
|
+
4. [Anti-Bot Protection](#-anti-bot-protection)
|
|
84
|
+
5. [Interceptors](#-interceptors)
|
|
85
|
+
6. [Metrics](#-metrics)
|
|
86
|
+
7. [TypeScript Support](#-typescript-support)
|
|
87
|
+
8. [API Reference](#-api-reference)
|
|
88
|
+
|
|
89
|
+
---
|
|
60
90
|
|
|
61
|
-
|
|
91
|
+
## 🌐 Making Requests
|
|
92
|
+
|
|
93
|
+
### GET Requests
|
|
62
94
|
|
|
63
95
|
```typescript
|
|
64
|
-
|
|
96
|
+
// Simple GET
|
|
97
|
+
const response = await client.url.api.get();
|
|
65
98
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
// GET with path
|
|
100
|
+
const user = await client.url.github.get({ path: "/users/octocat" });
|
|
101
|
+
|
|
102
|
+
// GET with headers
|
|
103
|
+
const data = await client.url.api.get({
|
|
104
|
+
path: "/protected",
|
|
105
|
+
headers: { Authorization: "Bearer token123" },
|
|
71
106
|
});
|
|
72
107
|
```
|
|
73
108
|
|
|
74
|
-
###
|
|
109
|
+
### POST, PUT, PATCH, DELETE
|
|
75
110
|
|
|
76
111
|
```typescript
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
// POST
|
|
113
|
+
await client.url.api.post(JSON.stringify({ name: "Jane" }), {
|
|
114
|
+
path: "/users",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// PUT
|
|
119
|
+
await client.url.api.put(JSON.stringify({ name: "Updated" }), {
|
|
120
|
+
path: "/users/123",
|
|
121
|
+
});
|
|
80
122
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
path: "/users/
|
|
84
|
-
headers: { Authorization: "token YOUR_TOKEN" },
|
|
123
|
+
// PATCH
|
|
124
|
+
await client.url.api.patch(JSON.stringify({ email: "new@email.com" }), {
|
|
125
|
+
path: "/users/123",
|
|
85
126
|
});
|
|
127
|
+
|
|
128
|
+
// DELETE
|
|
129
|
+
await client.url.api.delete(null, { path: "/users/123" });
|
|
86
130
|
```
|
|
87
131
|
|
|
88
|
-
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 📤 Response Handling
|
|
135
|
+
|
|
136
|
+
### Manual Parsing
|
|
89
137
|
|
|
90
138
|
```typescript
|
|
91
139
|
const response = await client.url.api.get({ path: "/data" });
|
|
92
140
|
|
|
93
|
-
//
|
|
141
|
+
// Parse as JSON
|
|
94
142
|
const json = await response.body.json();
|
|
95
143
|
|
|
96
|
-
//
|
|
144
|
+
// Parse as text
|
|
97
145
|
const text = await response.body.text();
|
|
98
146
|
|
|
99
|
-
//
|
|
147
|
+
// Get as buffer
|
|
100
148
|
const buffer = await response.body.arrayBuffer();
|
|
101
149
|
```
|
|
102
150
|
|
|
103
|
-
###
|
|
151
|
+
### Auto-Parse (Recommended)
|
|
152
|
+
|
|
153
|
+
Let Jiren parse the response automatically:
|
|
104
154
|
|
|
105
155
|
```typescript
|
|
106
|
-
//
|
|
156
|
+
// Auto-parse JSON
|
|
107
157
|
const users = await client.url.api.get({
|
|
108
158
|
path: "/users",
|
|
109
|
-
responseType: "json", // Returns parsed
|
|
159
|
+
responseType: "json", // ← Returns parsed data directly!
|
|
110
160
|
});
|
|
111
161
|
|
|
112
|
-
//
|
|
113
|
-
const html = await client.url.
|
|
114
|
-
|
|
162
|
+
// Auto-parse text
|
|
163
|
+
const html = await client.url.api.get({
|
|
164
|
+
path: "/page",
|
|
165
|
+
responseType: "text",
|
|
115
166
|
});
|
|
116
167
|
```
|
|
117
168
|
|
|
118
|
-
###
|
|
169
|
+
### Response Properties
|
|
119
170
|
|
|
120
171
|
```typescript
|
|
121
|
-
|
|
122
|
-
const created = await client.url.api.post(
|
|
123
|
-
JSON.stringify({ name: "John Doe", email: "john@example.com" }),
|
|
124
|
-
{
|
|
125
|
-
path: "/users",
|
|
126
|
-
headers: { "Content-Type": "application/json" },
|
|
127
|
-
responseType: "json",
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// PUT request
|
|
132
|
-
await client.url.api.put(JSON.stringify({ name: "Jane Doe" }), {
|
|
133
|
-
path: "/users/123",
|
|
134
|
-
headers: { "Content-Type": "application/json" },
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// PATCH request
|
|
138
|
-
await client.url.api.patch(JSON.stringify({ email: "new@example.com" }), {
|
|
139
|
-
path: "/users/123",
|
|
140
|
-
});
|
|
172
|
+
const response = await client.url.api.get({ path: "/users" });
|
|
141
173
|
|
|
142
|
-
//
|
|
143
|
-
|
|
174
|
+
console.log(response.status); // 200
|
|
175
|
+
console.log(response.ok); // true
|
|
176
|
+
console.log(response.headers); // { "content-type": "application/json", ... }
|
|
177
|
+
console.log(response.redirected); // false
|
|
144
178
|
```
|
|
145
179
|
|
|
146
180
|
---
|
|
147
181
|
|
|
148
|
-
##
|
|
182
|
+
## 💾 Caching
|
|
149
183
|
|
|
150
|
-
Enable
|
|
184
|
+
Enable caching for instant responses on repeated requests:
|
|
151
185
|
|
|
152
|
-
###
|
|
186
|
+
### Enable Caching
|
|
153
187
|
|
|
154
188
|
```typescript
|
|
155
189
|
const client = new JirenClient({
|
|
156
|
-
|
|
190
|
+
targets: {
|
|
157
191
|
api: {
|
|
158
192
|
url: "https://api.example.com",
|
|
159
|
-
cache: true, // Enable
|
|
193
|
+
cache: true, // ← Enable caching (60s default)
|
|
160
194
|
},
|
|
161
195
|
},
|
|
162
196
|
});
|
|
163
|
-
|
|
164
|
-
// First request: Real HTTP request (~150ms)
|
|
165
|
-
const data1 = await client.url.api.get({ path: "/users" });
|
|
166
|
-
|
|
167
|
-
// Second request: From cache (~1-2ms) - 100x faster! ⚡
|
|
168
|
-
const data2 = await client.url.api.get({ path: "/users" });
|
|
169
197
|
```
|
|
170
198
|
|
|
171
|
-
### Custom Cache
|
|
199
|
+
### Custom Cache Duration
|
|
172
200
|
|
|
173
201
|
```typescript
|
|
174
202
|
const client = new JirenClient({
|
|
175
|
-
|
|
203
|
+
targets: {
|
|
176
204
|
api: {
|
|
177
205
|
url: "https://api.example.com",
|
|
178
|
-
cache: { ttl: 300000 }, // 5
|
|
206
|
+
cache: { ttl: 300000 }, // 5 minutes
|
|
179
207
|
},
|
|
180
208
|
cdn: {
|
|
181
209
|
url: "https://cdn.example.com",
|
|
182
|
-
cache: { ttl: 3600000 }, // 1
|
|
210
|
+
cache: { ttl: 3600000 }, // 1 hour
|
|
183
211
|
},
|
|
184
212
|
},
|
|
185
213
|
});
|
|
186
214
|
```
|
|
187
215
|
|
|
188
|
-
###
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// Refresh cache for a specific endpoint
|
|
192
|
-
await client.url.api.prefetch({ path: "/users" });
|
|
193
|
-
|
|
194
|
-
// Now the next request will use fresh data
|
|
195
|
-
const users = await client.url.api.get({ path: "/users" });
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Cache Features
|
|
216
|
+
### Cache Performance
|
|
199
217
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
| Request Type | Speed | Improvement |
|
|
219
|
+
| -------------- | ------ | ------------------ |
|
|
220
|
+
| First request | ~150ms | - |
|
|
221
|
+
| Cached request | ~1-2ms | **100x faster** ⚡ |
|
|
204
222
|
|
|
205
|
-
###
|
|
223
|
+
### 🚀 Performance Benchmark
|
|
206
224
|
|
|
207
|
-
|
|
208
|
-
| ------------ | ------ | ----------------------------------------------- |
|
|
209
|
-
| Real request | ~150ms | First request or cache miss |
|
|
210
|
-
| File cache | ~1-2ms | Subsequent requests (same or different process) |
|
|
211
|
-
| Memory cache | ~0.1ms | Multiple requests in same process |
|
|
225
|
+
See the [top of this README](#-performance-fastest-in-the-world) for detailed benchmark results. Jiren consistently wins on throughput and tail latency.
|
|
212
226
|
|
|
213
|
-
|
|
227
|
+
### Refresh Cache
|
|
214
228
|
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
.
|
|
229
|
+
```typescript
|
|
230
|
+
// Force refresh cached data
|
|
231
|
+
await client.url.api.prefetch({ path: "/users" });
|
|
218
232
|
```
|
|
219
233
|
|
|
234
|
+
> 💡 **Tip:** Add `.cache/` to your `.gitignore`
|
|
235
|
+
|
|
220
236
|
---
|
|
221
237
|
|
|
222
|
-
## Anti-Bot Protection
|
|
238
|
+
## 🔒 Anti-Bot Protection
|
|
223
239
|
|
|
224
|
-
Bypass Cloudflare and other bot
|
|
240
|
+
Bypass Cloudflare and other bot protections:
|
|
225
241
|
|
|
226
242
|
```typescript
|
|
227
243
|
const client = new JirenClient({
|
|
228
|
-
|
|
229
|
-
protected: "https://
|
|
230
|
-
antibot: true,
|
|
244
|
+
targets: {
|
|
245
|
+
protected: "https://protected-site.com",
|
|
231
246
|
},
|
|
247
|
+
antibot: true,
|
|
232
248
|
});
|
|
233
249
|
|
|
234
|
-
// Enable anti-bot mode for this request
|
|
235
250
|
const response = await client.url.protected.get();
|
|
236
251
|
```
|
|
237
252
|
|
|
238
|
-
**How it works:**
|
|
239
|
-
|
|
240
|
-
- Uses `curl-impersonate` to mimic Chrome 120 browser
|
|
241
|
-
- Includes realistic TLS fingerprint and headers
|
|
242
|
-
- Bypasses most bot detection systems
|
|
243
|
-
|
|
244
253
|
---
|
|
245
254
|
|
|
246
|
-
##
|
|
255
|
+
## 🔄 Interceptors
|
|
247
256
|
|
|
248
|
-
Add middleware to
|
|
257
|
+
Add middleware to modify requests/responses:
|
|
249
258
|
|
|
250
|
-
###
|
|
259
|
+
### Add Authentication
|
|
251
260
|
|
|
252
261
|
```typescript
|
|
253
262
|
const client = new JirenClient({
|
|
254
|
-
|
|
263
|
+
targets: { api: "https://api.example.com" },
|
|
255
264
|
interceptors: {
|
|
256
|
-
// Modify requests before sending
|
|
257
265
|
request: [
|
|
258
266
|
(ctx) => ({
|
|
259
267
|
...ctx,
|
|
260
|
-
headers: {
|
|
268
|
+
headers: {
|
|
269
|
+
...ctx.headers,
|
|
270
|
+
Authorization: `Bearer ${getToken()}`,
|
|
271
|
+
},
|
|
261
272
|
}),
|
|
262
273
|
],
|
|
263
|
-
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Log Responses
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const client = new JirenClient({
|
|
282
|
+
targets: { api: "https://api.example.com" },
|
|
283
|
+
interceptors: {
|
|
264
284
|
response: [
|
|
265
285
|
(ctx) => {
|
|
266
|
-
console.log(
|
|
286
|
+
console.log(`${ctx.response.status} ${ctx.request.url}`);
|
|
267
287
|
return ctx;
|
|
268
288
|
},
|
|
269
289
|
],
|
|
270
|
-
// Handle errors
|
|
271
|
-
error: [(err, ctx) => console.error(`Failed: ${ctx.url}`, err)],
|
|
272
290
|
},
|
|
273
291
|
});
|
|
274
292
|
```
|
|
275
293
|
|
|
276
|
-
###
|
|
294
|
+
### Handle Errors
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const client = new JirenClient({
|
|
298
|
+
targets: { api: "https://api.example.com" },
|
|
299
|
+
interceptors: {
|
|
300
|
+
error: [
|
|
301
|
+
(error, ctx) => {
|
|
302
|
+
console.error(`Request failed: ${ctx.url}`);
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Add Interceptors Later
|
|
277
310
|
|
|
278
311
|
```typescript
|
|
279
|
-
// Add interceptors after client creation
|
|
280
312
|
client.use({
|
|
281
313
|
request: [
|
|
282
314
|
(ctx) => ({ ...ctx, headers: { ...ctx.headers, "X-Custom": "value" } }),
|
|
@@ -284,184 +316,47 @@ client.use({
|
|
|
284
316
|
});
|
|
285
317
|
```
|
|
286
318
|
|
|
287
|
-
### Interceptor Types
|
|
288
|
-
|
|
289
|
-
| Type | Purpose | Context |
|
|
290
|
-
| ---------- | ------------------------------------------------ | -------------------------------- |
|
|
291
|
-
| `request` | Modify method, URL, headers, body before sending | `{ method, url, headers, body }` |
|
|
292
|
-
| `response` | Transform response after receiving | `{ request, response }` |
|
|
293
|
-
| `error` | Handle errors centrally | `(error, requestContext)` |
|
|
294
|
-
|
|
295
319
|
---
|
|
296
320
|
|
|
297
|
-
## Metrics
|
|
321
|
+
## 📊 Metrics
|
|
298
322
|
|
|
299
|
-
Track performance
|
|
323
|
+
Track performance and cache efficiency:
|
|
300
324
|
|
|
301
|
-
###
|
|
325
|
+
### Get Endpoint Metrics
|
|
302
326
|
|
|
303
327
|
```typescript
|
|
304
|
-
const client = new JirenClient({
|
|
305
|
-
warmup: {
|
|
306
|
-
api: {
|
|
307
|
-
url: "https://api.example.com",
|
|
308
|
-
cache: true,
|
|
309
|
-
},
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Make some requests
|
|
314
|
-
await client.url.api.get({ path: "/users" });
|
|
315
|
-
await client.url.api.get({ path: "/users" }); // Cache hit
|
|
316
|
-
|
|
317
|
-
// Get endpoint metrics
|
|
318
328
|
const metrics = client.metrics.get("api");
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
requests: {
|
|
324
|
-
total: 2,
|
|
325
|
-
success: 2,
|
|
326
|
-
failed: 0
|
|
327
|
-
},
|
|
328
|
-
statusCodes: { 200: 2 },
|
|
329
|
-
timing: {
|
|
330
|
-
avgMs: 50.5,
|
|
331
|
-
minMs: 0.01,
|
|
332
|
-
maxMs: 101,
|
|
333
|
-
p50Ms: 50.5,
|
|
334
|
-
p95Ms: 101,
|
|
335
|
-
p99Ms: 101
|
|
336
|
-
},
|
|
337
|
-
cache: {
|
|
338
|
-
l1Hits: 1,
|
|
339
|
-
l1Misses: 1,
|
|
340
|
-
l2Hits: 0,
|
|
341
|
-
l2Misses: 1,
|
|
342
|
-
hitRate: "50.00%"
|
|
343
|
-
},
|
|
344
|
-
deduplication: {
|
|
345
|
-
hits: 0,
|
|
346
|
-
misses: 2,
|
|
347
|
-
hitRate: "0.00%"
|
|
348
|
-
},
|
|
349
|
-
bytes: {
|
|
350
|
-
sent: 0,
|
|
351
|
-
received: 0
|
|
352
|
-
},
|
|
353
|
-
errors: {},
|
|
354
|
-
lastRequestAt: 1234567890000
|
|
355
|
-
}
|
|
356
|
-
*/
|
|
329
|
+
|
|
330
|
+
console.log(metrics.requests.total); // Total requests made
|
|
331
|
+
console.log(metrics.timing.avgMs); // Average response time
|
|
332
|
+
console.log(metrics.cache.hitRate); // Cache hit percentage
|
|
357
333
|
```
|
|
358
334
|
|
|
359
|
-
### Global Metrics
|
|
335
|
+
### Get Global Metrics
|
|
360
336
|
|
|
361
337
|
```typescript
|
|
362
|
-
// Get aggregated metrics across all endpoints
|
|
363
338
|
const global = client.metrics.getGlobal();
|
|
364
|
-
console.log(global);
|
|
365
|
-
/*
|
|
366
|
-
{
|
|
367
|
-
totalRequests: 8,
|
|
368
|
-
totalSuccess: 8,
|
|
369
|
-
totalFailed: 0,
|
|
370
|
-
avgResponseTimeMs: 125.5,
|
|
371
|
-
totalBytesSent: 1024,
|
|
372
|
-
totalBytesReceived: 4096,
|
|
373
|
-
overallCacheHitRate: "62.50%",
|
|
374
|
-
overallDeduplicationRate: "25.00%",
|
|
375
|
-
endpoints: 2,
|
|
376
|
-
uptime: 60000
|
|
377
|
-
}
|
|
378
|
-
*/
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### All Endpoints
|
|
382
339
|
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
for (const [endpoint, metrics] of Object.entries(allMetrics)) {
|
|
387
|
-
console.log(`${endpoint}: ${metrics.cache.hitRate} cache hit rate`);
|
|
388
|
-
}
|
|
340
|
+
console.log(global.totalRequests); // All requests
|
|
341
|
+
console.log(global.avgResponseTimeMs); // Average across all endpoints
|
|
342
|
+
console.log(global.overallCacheHitRate); // Overall cache performance
|
|
389
343
|
```
|
|
390
344
|
|
|
391
|
-
### Export
|
|
345
|
+
### Export & Reset
|
|
392
346
|
|
|
393
347
|
```typescript
|
|
394
|
-
// Export
|
|
348
|
+
// Export as JSON
|
|
395
349
|
const json = client.metrics.export();
|
|
396
|
-
console.log(json); // Pretty-printed JSON string
|
|
397
350
|
|
|
398
|
-
//
|
|
399
|
-
await Bun.write("metrics.json", json);
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
### Reset Metrics
|
|
403
|
-
|
|
404
|
-
```typescript
|
|
405
|
-
// Reset specific endpoint
|
|
406
|
-
client.metrics.reset("api");
|
|
407
|
-
|
|
408
|
-
// Reset all metrics
|
|
351
|
+
// Reset metrics
|
|
409
352
|
client.metrics.reset();
|
|
410
353
|
```
|
|
411
354
|
|
|
412
|
-
### Tracked Metrics
|
|
413
|
-
|
|
414
|
-
| Category | Metrics |
|
|
415
|
-
| ------------ | ---------------------------------------------------------- |
|
|
416
|
-
| **Requests** | Total, success, failed, status code distribution |
|
|
417
|
-
| **Timing** | Average, min, max, P50, P95, P99 response times |
|
|
418
|
-
| **Cache** | L1/L2 hits & misses, overall hit rate |
|
|
419
|
-
| **Dedupe** | Deduplication hits/misses for identical in-flight requests |
|
|
420
|
-
| **Bytes** | Total sent/received |
|
|
421
|
-
| **Errors** | Error counts by type |
|
|
422
|
-
| **Other** | Last request timestamp, client uptime |
|
|
423
|
-
|
|
424
|
-
### Use Cases
|
|
425
|
-
|
|
426
|
-
**Performance Monitoring:**
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
|
-
// Track P99 latency
|
|
430
|
-
setInterval(() => {
|
|
431
|
-
const metrics = client.metrics.getGlobal();
|
|
432
|
-
if (metrics.avgResponseTimeMs > 1000) {
|
|
433
|
-
console.warn("High latency detected!");
|
|
434
|
-
}
|
|
435
|
-
}, 60000);
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**Cache Optimization:**
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
// Monitor cache effectiveness
|
|
442
|
-
const metrics = client.metrics.get("api");
|
|
443
|
-
console.log(`Cache hit rate: ${metrics.cache.hitRate}`);
|
|
444
|
-
if (parseFloat(metrics.cache.hitRate) < 50) {
|
|
445
|
-
console.log("Consider increasing cache TTL");
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**Debugging:**
|
|
450
|
-
|
|
451
|
-
```typescript
|
|
452
|
-
// Export metrics for debugging
|
|
453
|
-
if (process.env.DEBUG) {
|
|
454
|
-
process.on("exit", () => {
|
|
455
|
-
console.log(client.metrics.export());
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
```
|
|
459
|
-
|
|
460
355
|
---
|
|
461
356
|
|
|
462
|
-
##
|
|
357
|
+
## 🔷 TypeScript Support
|
|
463
358
|
|
|
464
|
-
###
|
|
359
|
+
### Type-Safe Responses
|
|
465
360
|
|
|
466
361
|
```typescript
|
|
467
362
|
interface User {
|
|
@@ -470,194 +365,92 @@ interface User {
|
|
|
470
365
|
email: string;
|
|
471
366
|
}
|
|
472
367
|
|
|
473
|
-
//
|
|
368
|
+
// TypeScript knows the response type!
|
|
474
369
|
const user = await client.url.api.get<User>({
|
|
475
370
|
path: "/users/123",
|
|
476
371
|
responseType: "json",
|
|
477
372
|
});
|
|
478
373
|
|
|
479
|
-
console.log(user.name); //
|
|
374
|
+
console.log(user.name); // ✅ Autocomplete works!
|
|
480
375
|
```
|
|
481
376
|
|
|
482
|
-
###
|
|
377
|
+
### Typed URL Keys
|
|
483
378
|
|
|
484
379
|
```typescript
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
warmup: {
|
|
380
|
+
const client = new JirenClient({
|
|
381
|
+
targets: {
|
|
488
382
|
api: "https://api.example.com",
|
|
489
383
|
cdn: "https://cdn.example.com",
|
|
490
384
|
},
|
|
491
385
|
});
|
|
492
386
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
{ key: "api", url: "https://api.example.com", cache: true },
|
|
497
|
-
{ key: "cdn", url: "https://cdn.example.com", cache: { ttl: 3600000 } },
|
|
498
|
-
],
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
// Simple array
|
|
502
|
-
const client3 = new JirenClient({
|
|
503
|
-
warmup: ["https://api.example.com", "https://cdn.example.com"],
|
|
504
|
-
});
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
### Benchmark Mode
|
|
508
|
-
|
|
509
|
-
Force HTTP/2 for consistent benchmarking:
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
const client = new JirenClient({
|
|
513
|
-
warmup: { api: "https://api.example.com" },
|
|
514
|
-
benchmark: true, // Disables HTTP/3 probing, forces HTTP/2
|
|
515
|
-
});
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
### Close Client
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
// Clean up resources when done
|
|
522
|
-
client.close();
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
---
|
|
526
|
-
|
|
527
|
-
## Performance
|
|
528
|
-
|
|
529
|
-
### Benchmark Results
|
|
530
|
-
|
|
387
|
+
client.url.api.get(); // ✅ Valid
|
|
388
|
+
client.url.cdn.get(); // ✅ Valid
|
|
389
|
+
client.url.foo.get(); // ❌ TypeScript error!
|
|
531
390
|
```
|
|
532
|
-
Bun fetch: ~300ms
|
|
533
|
-
JirenClient: ~120ms (2.5x faster)
|
|
534
|
-
JirenClient (cached): ~1-2ms (150x faster)
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Performance Tips
|
|
538
|
-
|
|
539
|
-
1. **Always use warmup** - Pre-establishes connections
|
|
540
|
-
2. **Enable caching** - For data that doesn't change frequently
|
|
541
|
-
3. **Reuse client instances** - Don't create new clients for each request
|
|
542
|
-
4. **Use `responseType`** - Automatic parsing is faster
|
|
543
|
-
5. **Batch requests** - Use `Promise.all()` for concurrent requests
|
|
544
|
-
|
|
545
|
-
### Why So Fast?
|
|
546
|
-
|
|
547
|
-
- **Native Zig implementation** - No JavaScript overhead
|
|
548
|
-
- **HTTP/3 (QUIC) support** - Faster than HTTP/2 when available
|
|
549
|
-
- **Connection pooling** - Reuses TCP/TLS connections
|
|
550
|
-
- **Smart caching** - Compressed persistent cache
|
|
551
|
-
- **Zero-copy operations** - Minimal memory allocations
|
|
552
|
-
- **TCP keep-alive** - Prevents connection drops
|
|
553
391
|
|
|
554
392
|
---
|
|
555
393
|
|
|
556
|
-
## API Reference
|
|
557
|
-
|
|
558
|
-
### `JirenClient` Constructor
|
|
559
|
-
|
|
560
|
-
```typescript
|
|
561
|
-
new JirenClient(options?: JirenClientOptions)
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
**Options:**
|
|
565
|
-
|
|
566
|
-
| Option | Type | Description |
|
|
567
|
-
| ----------- | -------------------------------------------------- | ---------------------------- |
|
|
568
|
-
| `warmup` | `Record<string, UrlConfig>` \| `WarmupUrlConfig[]` | URLs to pre-warm (required) |
|
|
569
|
-
| `benchmark` | `boolean` | Force HTTP/2 mode (optional) |
|
|
394
|
+
## 📚 API Reference
|
|
570
395
|
|
|
571
|
-
|
|
396
|
+
### Creating a Client
|
|
572
397
|
|
|
573
398
|
```typescript
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
cache?: boolean | { ttl: number };
|
|
579
|
-
};
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
### URL Endpoint Methods
|
|
583
|
-
|
|
584
|
-
All methods are available on `client.url.<key>`:
|
|
585
|
-
|
|
586
|
-
#### `get(options?)`
|
|
587
|
-
|
|
588
|
-
```typescript
|
|
589
|
-
get<T>(options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
#### `post(body?, options?)`
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
post<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
#### `put(body?, options?)`
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
put<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
#### `patch(body?, options?)`
|
|
605
|
-
|
|
606
|
-
```typescript
|
|
607
|
-
patch<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
#### `delete(body?, options?)`
|
|
611
|
-
|
|
612
|
-
```typescript
|
|
613
|
-
delete<T>(body?: string | null, options?: UrlRequestOptions): Promise<JirenResponse<T>>
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
#### `head(options?)`
|
|
617
|
-
|
|
618
|
-
```typescript
|
|
619
|
-
head(options?: UrlRequestOptions): Promise<JirenResponse<any>>
|
|
620
|
-
```
|
|
399
|
+
new JirenClient({
|
|
400
|
+
targets: {
|
|
401
|
+
// Simple URL
|
|
402
|
+
api: "https://api.example.com",
|
|
621
403
|
|
|
622
|
-
|
|
404
|
+
// With caching
|
|
405
|
+
cdn: {
|
|
406
|
+
url: "https://cdn.example.com",
|
|
407
|
+
cache: true, // or { ttl: 60000 }
|
|
408
|
+
},
|
|
409
|
+
},
|
|
623
410
|
|
|
624
|
-
|
|
625
|
-
|
|
411
|
+
// Optional settings
|
|
412
|
+
antibot: false, // Enable anti-bot protection
|
|
413
|
+
benchmark: false, // Benchmark mode
|
|
414
|
+
interceptors: {}, // Request/response interceptors
|
|
415
|
+
});
|
|
626
416
|
```
|
|
627
417
|
|
|
628
|
-
|
|
418
|
+
### Request Methods
|
|
629
419
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
420
|
+
| Method | Signature |
|
|
421
|
+
| ------------ | -------------------------------------------------- |
|
|
422
|
+
| `get()` | `get<T>(options?): Promise<Response<T>>` |
|
|
423
|
+
| `post()` | `post<T>(body?, options?): Promise<Response<T>>` |
|
|
424
|
+
| `put()` | `put<T>(body?, options?): Promise<Response<T>>` |
|
|
425
|
+
| `patch()` | `patch<T>(body?, options?): Promise<Response<T>>` |
|
|
426
|
+
| `delete()` | `delete<T>(body?, options?): Promise<Response<T>>` |
|
|
427
|
+
| `head()` | `head(options?): Promise<Response>` |
|
|
428
|
+
| `options()` | `options(options?): Promise<Response>` |
|
|
429
|
+
| `prefetch()` | `prefetch(options?): Promise<void>` |
|
|
635
430
|
|
|
636
431
|
### Request Options
|
|
637
432
|
|
|
638
433
|
```typescript
|
|
639
|
-
|
|
640
|
-
path?: string;
|
|
434
|
+
{
|
|
435
|
+
path?: string; // URL path to append
|
|
641
436
|
headers?: Record<string, string>; // Request headers
|
|
642
|
-
responseType?: "json" | "text";
|
|
643
|
-
|
|
644
|
-
maxRedirects?: number; // Max redirects to follow
|
|
437
|
+
responseType?: "json" | "text"; // Auto-parse response
|
|
438
|
+
maxRedirects?: number; // Max redirects (default: 5)
|
|
645
439
|
}
|
|
646
440
|
```
|
|
647
441
|
|
|
648
442
|
### Response Object
|
|
649
443
|
|
|
650
444
|
```typescript
|
|
651
|
-
|
|
445
|
+
{
|
|
652
446
|
url: string;
|
|
653
447
|
status: number;
|
|
654
448
|
statusText: string;
|
|
655
449
|
headers: Record<string, string>;
|
|
656
450
|
ok: boolean;
|
|
657
451
|
redirected: boolean;
|
|
658
|
-
type: string;
|
|
659
452
|
body: {
|
|
660
|
-
json<
|
|
453
|
+
json<T>(): Promise<T>;
|
|
661
454
|
text(): Promise<string>;
|
|
662
455
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
663
456
|
blob(): Promise<Blob>;
|
|
@@ -667,89 +460,132 @@ interface JirenResponse<T = any> {
|
|
|
667
460
|
|
|
668
461
|
---
|
|
669
462
|
|
|
670
|
-
## Examples
|
|
463
|
+
## 💡 Examples
|
|
671
464
|
|
|
672
|
-
###
|
|
465
|
+
### API Client Pattern
|
|
673
466
|
|
|
674
467
|
```typescript
|
|
675
|
-
// api
|
|
468
|
+
// lib/api.ts
|
|
676
469
|
import { JirenClient } from "jiren";
|
|
677
470
|
|
|
678
|
-
export const
|
|
679
|
-
|
|
471
|
+
export const api = new JirenClient({
|
|
472
|
+
targets: {
|
|
680
473
|
backend: {
|
|
681
|
-
url:
|
|
682
|
-
cache:
|
|
683
|
-
},
|
|
684
|
-
cdn: {
|
|
685
|
-
url: "https://cdn.myapp.com",
|
|
686
|
-
cache: { ttl: 3600000 }, // 1-hour cache for static assets
|
|
474
|
+
url: process.env.API_URL!,
|
|
475
|
+
cache: { ttl: 30000 },
|
|
687
476
|
},
|
|
688
477
|
},
|
|
478
|
+
interceptors: {
|
|
479
|
+
request: [
|
|
480
|
+
(ctx) => ({
|
|
481
|
+
...ctx,
|
|
482
|
+
headers: {
|
|
483
|
+
...ctx.headers,
|
|
484
|
+
Authorization: `Bearer ${getSession()?.token}`,
|
|
485
|
+
},
|
|
486
|
+
}),
|
|
487
|
+
],
|
|
488
|
+
},
|
|
689
489
|
});
|
|
690
490
|
|
|
691
|
-
//
|
|
692
|
-
import {
|
|
491
|
+
// Usage anywhere
|
|
492
|
+
import { api } from "@/lib/api";
|
|
493
|
+
const users = await api.url.backend.get({
|
|
494
|
+
path: "/users",
|
|
495
|
+
responseType: "json",
|
|
496
|
+
});
|
|
497
|
+
```
|
|
693
498
|
|
|
694
|
-
|
|
695
|
-
|
|
499
|
+
### React/Next.js Hook
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
function useApi<T>(path: string) {
|
|
503
|
+
const [data, setData] = useState<T | null>(null);
|
|
504
|
+
const [loading, setLoading] = useState(true);
|
|
696
505
|
|
|
697
506
|
useEffect(() => {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
.
|
|
701
|
-
.
|
|
702
|
-
}, []);
|
|
507
|
+
api.url.backend
|
|
508
|
+
.get<T>({ path, responseType: "json" })
|
|
509
|
+
.then(setData)
|
|
510
|
+
.finally(() => setLoading(false));
|
|
511
|
+
}, [path]);
|
|
512
|
+
|
|
513
|
+
return { data, loading };
|
|
514
|
+
}
|
|
703
515
|
|
|
704
|
-
|
|
516
|
+
// Usage
|
|
517
|
+
function UserList() {
|
|
518
|
+
const { data: users, loading } = useApi<User[]>("/users");
|
|
519
|
+
if (loading) return <Spinner />;
|
|
520
|
+
return (
|
|
521
|
+
<ul>
|
|
522
|
+
{users?.map((u) => (
|
|
523
|
+
<li key={u.id}>{u.name}</li>
|
|
524
|
+
))}
|
|
525
|
+
</ul>
|
|
526
|
+
);
|
|
705
527
|
}
|
|
706
528
|
```
|
|
707
529
|
|
|
708
|
-
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 🏗️ Framework Integration
|
|
533
|
+
|
|
534
|
+
### Next.js (App Router)
|
|
535
|
+
|
|
536
|
+
To use Jiren in Next.js, add it to `serverExternalPackages` in `next.config.ts`:
|
|
709
537
|
|
|
710
538
|
```typescript
|
|
539
|
+
import type { NextConfig } from "next";
|
|
540
|
+
|
|
541
|
+
const nextConfig: NextConfig = {
|
|
542
|
+
// Required: Treat Jiren and Koffi as external native packages
|
|
543
|
+
serverExternalPackages: ["jiren", "koffi"],
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
export default nextConfig;
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Usage in API Routes:**
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
// app/api/data/route.ts
|
|
553
|
+
import { JirenClient } from "jiren";
|
|
554
|
+
|
|
711
555
|
const client = new JirenClient({
|
|
712
|
-
|
|
713
|
-
api: {
|
|
714
|
-
url: "https://api.example.com",
|
|
715
|
-
cache: { ttl: 30000 }, // 30-second cache
|
|
716
|
-
},
|
|
717
|
-
},
|
|
556
|
+
targets: [{ key: "api", url: "https://api.example.com" }],
|
|
718
557
|
});
|
|
719
558
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
path: "/notifications",
|
|
725
|
-
responseType: "json",
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
updateUI(notifications);
|
|
729
|
-
}, 10000);
|
|
559
|
+
export async function GET() {
|
|
560
|
+
const response = await client.url.api.get({ path: "/users" });
|
|
561
|
+
return Response.json(await response.body.json());
|
|
562
|
+
}
|
|
730
563
|
```
|
|
731
564
|
|
|
732
565
|
---
|
|
733
566
|
|
|
734
|
-
## Requirements
|
|
567
|
+
## 📋 Requirements
|
|
735
568
|
|
|
736
|
-
- **
|
|
569
|
+
- **Runtime**:
|
|
570
|
+
- **Bun** v1.0.0+
|
|
571
|
+
- **Node.js** v18.0.0+ (macOS/Linux)
|
|
572
|
+
- **OS**: macOS (ARM64/x64) or Linux (x64)
|
|
737
573
|
|
|
738
574
|
---
|
|
739
575
|
|
|
740
|
-
## License
|
|
576
|
+
## 📄 License
|
|
741
577
|
|
|
742
|
-
MIT ©
|
|
578
|
+
MIT © VK
|
|
743
579
|
|
|
744
580
|
---
|
|
745
581
|
|
|
746
|
-
## Contributing
|
|
582
|
+
## 🤝 Contributing
|
|
747
583
|
|
|
748
|
-
Contributions
|
|
584
|
+
Contributions welcome! Please open an issue or submit a pull request.
|
|
749
585
|
|
|
750
586
|
---
|
|
751
587
|
|
|
752
|
-
## Support
|
|
588
|
+
## 🆘 Support
|
|
753
589
|
|
|
754
590
|
- 📖 [Documentation](https://github.com/vikashkhati007/jiren)
|
|
755
591
|
- 🐛 [Issue Tracker](https://github.com/vikashkhati007/jiren/issues)
|
|
@@ -757,4 +593,6 @@ Contributions are welcome! Please open an issue or submit a pull request.
|
|
|
757
593
|
|
|
758
594
|
---
|
|
759
595
|
|
|
760
|
-
|
|
596
|
+
<p align="center">
|
|
597
|
+
<strong>Made with ⚡ by VK</strong>
|
|
598
|
+
</p>
|