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
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ console.log(users);
|
|
|
46
46
|
- [Basic Usage](#basic-usage)
|
|
47
47
|
- [Response Caching](#response-caching)
|
|
48
48
|
- [Anti-Bot Protection](#anti-bot-protection)
|
|
49
|
+
- [Request Interceptors](#request-interceptors)
|
|
49
50
|
- [Advanced Features](#advanced-features)
|
|
50
51
|
- [Performance](#performance)
|
|
51
52
|
- [API Reference](#api-reference)
|
|
@@ -242,6 +243,57 @@ const response = await client.url.protected.get({
|
|
|
242
243
|
|
|
243
244
|
---
|
|
244
245
|
|
|
246
|
+
## Request Interceptors
|
|
247
|
+
|
|
248
|
+
Add middleware to intercept requests and responses for logging, auth injection, and more:
|
|
249
|
+
|
|
250
|
+
### Basic Interceptors
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
const client = new JirenClient({
|
|
254
|
+
warmup: { api: "https://api.example.com" },
|
|
255
|
+
interceptors: {
|
|
256
|
+
// Modify requests before sending
|
|
257
|
+
request: [
|
|
258
|
+
(ctx) => ({
|
|
259
|
+
...ctx,
|
|
260
|
+
headers: { ...ctx.headers, Authorization: `Bearer ${getToken()}` },
|
|
261
|
+
}),
|
|
262
|
+
],
|
|
263
|
+
// Transform responses after receiving
|
|
264
|
+
response: [
|
|
265
|
+
(ctx) => {
|
|
266
|
+
console.log(`[${ctx.response.status}] ${ctx.request.url}`);
|
|
267
|
+
return ctx;
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
// Handle errors
|
|
271
|
+
error: [(err, ctx) => console.error(`Failed: ${ctx.url}`, err)],
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Dynamic Interceptors with `use()`
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// Add interceptors after client creation
|
|
280
|
+
client.use({
|
|
281
|
+
request: [
|
|
282
|
+
(ctx) => ({ ...ctx, headers: { ...ctx.headers, "X-Custom": "value" } }),
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
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
|
+
---
|
|
296
|
+
|
|
245
297
|
## Advanced Features
|
|
246
298
|
|
|
247
299
|
### TypeScript Generics
|
package/components/cache.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from "fs";
|
|
2
10
|
import { gzipSync, gunzipSync } from "zlib";
|
|
3
11
|
import { createHash } from "crypto";
|
|
4
12
|
import { join } from "path";
|
|
5
|
-
import type { JirenResponse } from "./types";
|
|
13
|
+
import type { JirenResponse, JirenResponseBody } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serializable cache entry - stores raw body data for reconstruction
|
|
17
|
+
*/
|
|
18
|
+
interface SerializableCacheEntry {
|
|
19
|
+
// Store serializable parts of response
|
|
20
|
+
status: number;
|
|
21
|
+
statusText: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
url: string;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
redirected: boolean;
|
|
26
|
+
type: "basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect";
|
|
27
|
+
// Store raw body as base64 string (for binary compatibility)
|
|
28
|
+
bodyData: string;
|
|
29
|
+
bodyIsText: boolean;
|
|
30
|
+
// Cache metadata
|
|
31
|
+
timestamp: number;
|
|
32
|
+
ttl: number;
|
|
33
|
+
}
|
|
6
34
|
|
|
7
35
|
interface CacheEntry {
|
|
8
36
|
response: JirenResponse;
|
|
@@ -10,12 +38,224 @@ interface CacheEntry {
|
|
|
10
38
|
ttl: number;
|
|
11
39
|
}
|
|
12
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Reconstruct body methods from raw data
|
|
43
|
+
*/
|
|
44
|
+
function reconstructBody(bodyData: string, isText: boolean): JirenResponseBody {
|
|
45
|
+
let buffer: ArrayBuffer;
|
|
46
|
+
|
|
47
|
+
if (isText) {
|
|
48
|
+
// Text data stored directly
|
|
49
|
+
const encoder = new TextEncoder();
|
|
50
|
+
buffer = encoder.encode(bodyData).buffer as ArrayBuffer;
|
|
51
|
+
} else {
|
|
52
|
+
// Binary data stored as base64
|
|
53
|
+
const binaryString = atob(bodyData);
|
|
54
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
55
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
56
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
57
|
+
}
|
|
58
|
+
buffer = bytes.buffer as ArrayBuffer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
bodyUsed: false,
|
|
63
|
+
text: async () => {
|
|
64
|
+
if (isText) return bodyData;
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
return decoder.decode(buffer);
|
|
67
|
+
},
|
|
68
|
+
json: async () => {
|
|
69
|
+
const text = isText ? bodyData : new TextDecoder().decode(buffer);
|
|
70
|
+
return JSON.parse(text);
|
|
71
|
+
},
|
|
72
|
+
arrayBuffer: async () => buffer,
|
|
73
|
+
blob: async () => new Blob([buffer]),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* L1 In-Memory LRU Cache Node
|
|
79
|
+
*/
|
|
80
|
+
interface LRUNode {
|
|
81
|
+
key: string;
|
|
82
|
+
entry: CacheEntry;
|
|
83
|
+
prev: LRUNode | null;
|
|
84
|
+
next: LRUNode | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* L1 In-Memory LRU Cache
|
|
89
|
+
* Provides ~0.001ms access time (vs ~5ms for disk)
|
|
90
|
+
*/
|
|
91
|
+
class L1MemoryCache {
|
|
92
|
+
private capacity: number;
|
|
93
|
+
private cache: Map<string, LRUNode> = new Map();
|
|
94
|
+
private head: LRUNode | null = null; // Most recently used
|
|
95
|
+
private tail: LRUNode | null = null; // Least recently used
|
|
96
|
+
|
|
97
|
+
// Stats
|
|
98
|
+
public hits = 0;
|
|
99
|
+
public misses = 0;
|
|
100
|
+
|
|
101
|
+
constructor(capacity = 100) {
|
|
102
|
+
this.capacity = capacity;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get from L1 cache (O(1) lookup + LRU update)
|
|
107
|
+
*/
|
|
108
|
+
get(key: string): CacheEntry | null {
|
|
109
|
+
const node = this.cache.get(key);
|
|
110
|
+
if (!node) {
|
|
111
|
+
this.misses++;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if expired
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
if (now - node.entry.timestamp > node.entry.ttl) {
|
|
118
|
+
this.delete(key);
|
|
119
|
+
this.misses++;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Move to front (most recently used)
|
|
124
|
+
this.moveToFront(node);
|
|
125
|
+
this.hits++;
|
|
126
|
+
return node.entry;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set in L1 cache with LRU eviction
|
|
131
|
+
*/
|
|
132
|
+
set(key: string, entry: CacheEntry): void {
|
|
133
|
+
// If key exists, update and move to front
|
|
134
|
+
const existing = this.cache.get(key);
|
|
135
|
+
if (existing) {
|
|
136
|
+
existing.entry = entry;
|
|
137
|
+
this.moveToFront(existing);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create new node
|
|
142
|
+
const node: LRUNode = {
|
|
143
|
+
key,
|
|
144
|
+
entry,
|
|
145
|
+
prev: null,
|
|
146
|
+
next: this.head,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Add to front
|
|
150
|
+
if (this.head) {
|
|
151
|
+
this.head.prev = node;
|
|
152
|
+
}
|
|
153
|
+
this.head = node;
|
|
154
|
+
if (!this.tail) {
|
|
155
|
+
this.tail = node;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.cache.set(key, node);
|
|
159
|
+
|
|
160
|
+
// Evict LRU if over capacity
|
|
161
|
+
if (this.cache.size > this.capacity) {
|
|
162
|
+
this.evictLRU();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete from L1 cache
|
|
168
|
+
*/
|
|
169
|
+
delete(key: string): void {
|
|
170
|
+
const node = this.cache.get(key);
|
|
171
|
+
if (!node) return;
|
|
172
|
+
|
|
173
|
+
this.removeNode(node);
|
|
174
|
+
this.cache.delete(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clear all L1 cache
|
|
179
|
+
*/
|
|
180
|
+
clear(): void {
|
|
181
|
+
this.cache.clear();
|
|
182
|
+
this.head = null;
|
|
183
|
+
this.tail = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get L1 cache stats
|
|
188
|
+
*/
|
|
189
|
+
stats() {
|
|
190
|
+
return {
|
|
191
|
+
size: this.cache.size,
|
|
192
|
+
capacity: this.capacity,
|
|
193
|
+
hits: this.hits,
|
|
194
|
+
misses: this.misses,
|
|
195
|
+
hitRate:
|
|
196
|
+
this.hits + this.misses > 0
|
|
197
|
+
? ((this.hits / (this.hits + this.misses)) * 100).toFixed(2) + "%"
|
|
198
|
+
: "0%",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private moveToFront(node: LRUNode): void {
|
|
203
|
+
if (node === this.head) return; // Already at front
|
|
204
|
+
|
|
205
|
+
this.removeNode(node);
|
|
206
|
+
|
|
207
|
+
// Add to front
|
|
208
|
+
node.prev = null;
|
|
209
|
+
node.next = this.head;
|
|
210
|
+
if (this.head) {
|
|
211
|
+
this.head.prev = node;
|
|
212
|
+
}
|
|
213
|
+
this.head = node;
|
|
214
|
+
if (!this.tail) {
|
|
215
|
+
this.tail = node;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private removeNode(node: LRUNode): void {
|
|
220
|
+
if (node.prev) {
|
|
221
|
+
node.prev.next = node.next;
|
|
222
|
+
} else {
|
|
223
|
+
this.head = node.next;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (node.next) {
|
|
227
|
+
node.next.prev = node.prev;
|
|
228
|
+
} else {
|
|
229
|
+
this.tail = node.prev;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private evictLRU(): void {
|
|
234
|
+
if (!this.tail) return;
|
|
235
|
+
|
|
236
|
+
const key = this.tail.key;
|
|
237
|
+
this.removeNode(this.tail);
|
|
238
|
+
this.cache.delete(key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Two-Tier Response Cache
|
|
244
|
+
*
|
|
245
|
+
* L1: In-Memory LRU Cache (~0.001ms access) - hot data
|
|
246
|
+
* L2: Disk Cache with gzip (~5ms access) - persistence
|
|
247
|
+
*
|
|
248
|
+
* Read path: L1 → L2 → Network
|
|
249
|
+
* Write path: L1 + L2 (write-through)
|
|
250
|
+
*/
|
|
13
251
|
export class ResponseCache {
|
|
252
|
+
private l1: L1MemoryCache;
|
|
14
253
|
private cacheDir: string;
|
|
15
|
-
private
|
|
254
|
+
private maxDiskSize: number;
|
|
16
255
|
|
|
17
|
-
constructor(
|
|
18
|
-
this.
|
|
256
|
+
constructor(l1Capacity = 100, cacheDir = ".cache/jiren", maxDiskSize = 100) {
|
|
257
|
+
this.l1 = new L1MemoryCache(l1Capacity);
|
|
258
|
+
this.maxDiskSize = maxDiskSize;
|
|
19
259
|
this.cacheDir = cacheDir;
|
|
20
260
|
|
|
21
261
|
// Create cache directory if it doesn't exist
|
|
@@ -45,10 +285,58 @@ export class ResponseCache {
|
|
|
45
285
|
}
|
|
46
286
|
|
|
47
287
|
/**
|
|
48
|
-
*
|
|
288
|
+
* Preload L2 disk cache entry into L1 memory cache.
|
|
289
|
+
* Call this during initialization to ensure first request hits L1.
|
|
290
|
+
* @param url - Base URL to preload
|
|
291
|
+
* @param path - Optional path
|
|
292
|
+
* @param options - Optional request options
|
|
293
|
+
* @returns true if entry was preloaded, false if not found/expired
|
|
294
|
+
*/
|
|
295
|
+
preloadL1(url: string, path?: string, options?: any): boolean {
|
|
296
|
+
const key = this.generateKey(url, path, options);
|
|
297
|
+
|
|
298
|
+
// Check if already in L1
|
|
299
|
+
if (this.l1.get(key)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Try to load from L2 disk
|
|
304
|
+
const l2Entry = this.getFromDisk(key);
|
|
305
|
+
if (l2Entry) {
|
|
306
|
+
this.l1.set(key, l2Entry);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get cached response - checks L1 first, then L2
|
|
49
315
|
*/
|
|
50
316
|
get(url: string, path?: string, options?: any): JirenResponse | null {
|
|
51
317
|
const key = this.generateKey(url, path, options);
|
|
318
|
+
|
|
319
|
+
// L1: Check in-memory cache first (~0.001ms)
|
|
320
|
+
const l1Entry = this.l1.get(key);
|
|
321
|
+
if (l1Entry) {
|
|
322
|
+
return l1Entry.response;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// L2: Check disk cache (~5ms)
|
|
326
|
+
const l2Entry = this.getFromDisk(key);
|
|
327
|
+
if (l2Entry) {
|
|
328
|
+
// Promote to L1 for faster future access
|
|
329
|
+
this.l1.set(key, l2Entry);
|
|
330
|
+
return l2Entry.response;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get from L2 disk cache
|
|
338
|
+
*/
|
|
339
|
+
private getFromDisk(key: string): CacheEntry | null {
|
|
52
340
|
const filePath = this.getCacheFilePath(key);
|
|
53
341
|
|
|
54
342
|
if (!existsSync(filePath)) return null;
|
|
@@ -60,30 +348,46 @@ export class ResponseCache {
|
|
|
60
348
|
// Decompress
|
|
61
349
|
const decompressed = gunzipSync(compressed);
|
|
62
350
|
const data = decompressed.toString("utf-8");
|
|
63
|
-
const
|
|
351
|
+
const serialized: SerializableCacheEntry = JSON.parse(data);
|
|
64
352
|
|
|
65
353
|
// Check if expired
|
|
66
354
|
const now = Date.now();
|
|
67
|
-
if (now -
|
|
355
|
+
if (now - serialized.timestamp > serialized.ttl) {
|
|
68
356
|
// Delete expired cache file
|
|
69
357
|
try {
|
|
70
|
-
|
|
358
|
+
unlinkSync(filePath);
|
|
71
359
|
} catch {}
|
|
72
360
|
return null;
|
|
73
361
|
}
|
|
74
362
|
|
|
75
|
-
|
|
363
|
+
// Reconstruct the response with working body methods
|
|
364
|
+
const response: JirenResponse = {
|
|
365
|
+
status: serialized.status,
|
|
366
|
+
statusText: serialized.statusText,
|
|
367
|
+
headers: serialized.headers,
|
|
368
|
+
url: serialized.url,
|
|
369
|
+
ok: serialized.ok,
|
|
370
|
+
redirected: serialized.redirected,
|
|
371
|
+
type: serialized.type || "default",
|
|
372
|
+
body: reconstructBody(serialized.bodyData, serialized.bodyIsText),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
response,
|
|
377
|
+
timestamp: serialized.timestamp,
|
|
378
|
+
ttl: serialized.ttl,
|
|
379
|
+
};
|
|
76
380
|
} catch (error) {
|
|
77
381
|
// Invalid cache file, delete it
|
|
78
382
|
try {
|
|
79
|
-
|
|
383
|
+
unlinkSync(filePath);
|
|
80
384
|
} catch {}
|
|
81
385
|
return null;
|
|
82
386
|
}
|
|
83
387
|
}
|
|
84
388
|
|
|
85
389
|
/**
|
|
86
|
-
* Store response in
|
|
390
|
+
* Store response in both L1 and L2 (write-through)
|
|
87
391
|
*/
|
|
88
392
|
set(
|
|
89
393
|
url: string,
|
|
@@ -93,7 +397,6 @@ export class ResponseCache {
|
|
|
93
397
|
options?: any
|
|
94
398
|
): void {
|
|
95
399
|
const key = this.generateKey(url, path, options);
|
|
96
|
-
const filePath = this.getCacheFilePath(key);
|
|
97
400
|
|
|
98
401
|
const entry: CacheEntry = {
|
|
99
402
|
response,
|
|
@@ -101,9 +404,59 @@ export class ResponseCache {
|
|
|
101
404
|
ttl,
|
|
102
405
|
};
|
|
103
406
|
|
|
407
|
+
// L1: Store in memory (~0.001ms)
|
|
408
|
+
this.l1.set(key, entry);
|
|
409
|
+
|
|
410
|
+
// L2: Store on disk (async-ish, for persistence)
|
|
411
|
+
this.setToDisk(key, entry);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Write to L2 disk cache
|
|
416
|
+
*/
|
|
417
|
+
private async setToDisk(key: string, entry: CacheEntry): Promise<void> {
|
|
418
|
+
const filePath = this.getCacheFilePath(key);
|
|
419
|
+
|
|
104
420
|
try {
|
|
421
|
+
// Extract body text for serialization
|
|
422
|
+
let bodyData: string;
|
|
423
|
+
let bodyIsText = true;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
bodyData = await entry.response.body.text();
|
|
427
|
+
} catch {
|
|
428
|
+
// If text fails, try arrayBuffer and convert to base64
|
|
429
|
+
try {
|
|
430
|
+
const buffer = await entry.response.body.arrayBuffer();
|
|
431
|
+
const bytes = new Uint8Array(buffer);
|
|
432
|
+
let binary = "";
|
|
433
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
434
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
435
|
+
}
|
|
436
|
+
bodyData = btoa(binary);
|
|
437
|
+
bodyIsText = false;
|
|
438
|
+
} catch {
|
|
439
|
+
bodyData = "";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Create serializable entry
|
|
444
|
+
const serialized: SerializableCacheEntry = {
|
|
445
|
+
status: entry.response.status,
|
|
446
|
+
statusText: entry.response.statusText,
|
|
447
|
+
headers: entry.response.headers as Record<string, string>,
|
|
448
|
+
url: entry.response.url,
|
|
449
|
+
ok: entry.response.ok,
|
|
450
|
+
redirected: entry.response.redirected,
|
|
451
|
+
type: entry.response.type || "default",
|
|
452
|
+
bodyData,
|
|
453
|
+
bodyIsText,
|
|
454
|
+
timestamp: entry.timestamp,
|
|
455
|
+
ttl: entry.ttl,
|
|
456
|
+
};
|
|
457
|
+
|
|
105
458
|
// Convert to JSON
|
|
106
|
-
const json = JSON.stringify(
|
|
459
|
+
const json = JSON.stringify(serialized);
|
|
107
460
|
|
|
108
461
|
// Compress with gzip
|
|
109
462
|
const compressed = gzipSync(json);
|
|
@@ -120,26 +473,29 @@ export class ResponseCache {
|
|
|
120
473
|
* Clear cache for a specific URL or all
|
|
121
474
|
*/
|
|
122
475
|
clear(url?: string): void {
|
|
476
|
+
// Clear L1
|
|
477
|
+
this.l1.clear();
|
|
478
|
+
|
|
479
|
+
// Clear L2
|
|
123
480
|
if (url) {
|
|
124
481
|
// Clear all cache files for this URL
|
|
125
482
|
// This is approximate since we hash the keys
|
|
126
483
|
// For now, just clear all to be safe
|
|
127
|
-
this.
|
|
484
|
+
this.clearAllDisk();
|
|
128
485
|
} else {
|
|
129
|
-
this.
|
|
486
|
+
this.clearAllDisk();
|
|
130
487
|
}
|
|
131
488
|
}
|
|
132
489
|
|
|
133
490
|
/**
|
|
134
|
-
* Clear all cache files
|
|
491
|
+
* Clear all L2 disk cache files
|
|
135
492
|
*/
|
|
136
|
-
private
|
|
493
|
+
private clearAllDisk(): void {
|
|
137
494
|
try {
|
|
138
|
-
const
|
|
139
|
-
const files = fs.readdirSync(this.cacheDir);
|
|
495
|
+
const files = readdirSync(this.cacheDir);
|
|
140
496
|
for (const file of files) {
|
|
141
497
|
if (file.endsWith(".json.gz")) {
|
|
142
|
-
|
|
498
|
+
unlinkSync(join(this.cacheDir, file));
|
|
143
499
|
}
|
|
144
500
|
}
|
|
145
501
|
} catch (error) {
|
|
@@ -148,34 +504,35 @@ export class ResponseCache {
|
|
|
148
504
|
}
|
|
149
505
|
|
|
150
506
|
/**
|
|
151
|
-
* Get cache statistics
|
|
507
|
+
* Get combined cache statistics
|
|
152
508
|
*/
|
|
153
509
|
stats() {
|
|
510
|
+
const l1Stats = this.l1.stats();
|
|
511
|
+
|
|
512
|
+
// L2 disk stats
|
|
513
|
+
let diskSize = 0;
|
|
514
|
+
let diskFiles = 0;
|
|
515
|
+
let totalDiskBytes = 0;
|
|
516
|
+
|
|
154
517
|
try {
|
|
155
|
-
const
|
|
156
|
-
const files = fs.readdirSync(this.cacheDir);
|
|
518
|
+
const files = readdirSync(this.cacheDir);
|
|
157
519
|
const cacheFiles = files.filter((f: string) => f.endsWith(".json.gz"));
|
|
520
|
+
diskFiles = cacheFiles.length;
|
|
158
521
|
|
|
159
|
-
// Calculate total size
|
|
160
|
-
let totalSize = 0;
|
|
161
522
|
for (const file of cacheFiles) {
|
|
162
|
-
const stats =
|
|
163
|
-
|
|
523
|
+
const stats = statSync(join(this.cacheDir, file));
|
|
524
|
+
totalDiskBytes += stats.size;
|
|
164
525
|
}
|
|
526
|
+
} catch {}
|
|
165
527
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
} catch {
|
|
173
|
-
return {
|
|
174
|
-
size: 0,
|
|
175
|
-
maxSize: this.maxSize,
|
|
528
|
+
return {
|
|
529
|
+
l1: l1Stats,
|
|
530
|
+
l2: {
|
|
531
|
+
size: diskFiles,
|
|
532
|
+
maxSize: this.maxDiskSize,
|
|
176
533
|
cacheDir: this.cacheDir,
|
|
177
|
-
totalSizeKB:
|
|
178
|
-
}
|
|
179
|
-
}
|
|
534
|
+
totalSizeKB: (totalDiskBytes / 1024).toFixed(2),
|
|
535
|
+
},
|
|
536
|
+
};
|
|
180
537
|
}
|
|
181
538
|
}
|