jiren 1.4.0 → 1.5.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 +220 -3
- package/components/cache.ts +398 -41
- package/components/client-node-native.ts +45 -12
- package/components/client.ts +524 -99
- package/components/index.ts +9 -0
- package/components/metrics.ts +420 -0
- package/components/native-cache.ts +181 -0
- package/components/native-node.ts +26 -0
- package/components/native.ts +92 -0
- package/components/types.ts +105 -5
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +1 -1
- package/components/client-node.ts +0 -440
package/components/index.ts
CHANGED
|
@@ -20,6 +20,15 @@ export type {
|
|
|
20
20
|
UrlEndpoint,
|
|
21
21
|
JirenResponse,
|
|
22
22
|
JirenResponseBody,
|
|
23
|
+
Interceptors,
|
|
24
|
+
RequestInterceptor,
|
|
25
|
+
ResponseInterceptor,
|
|
26
|
+
ErrorInterceptor,
|
|
27
|
+
InterceptorRequestContext,
|
|
28
|
+
InterceptorResponseContext,
|
|
29
|
+
EndpointMetrics,
|
|
30
|
+
GlobalMetrics,
|
|
31
|
+
MetricsAPI,
|
|
23
32
|
} from "./types";
|
|
24
33
|
|
|
25
34
|
// Remove broken exports
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics & Observability System for Jiren HTTP Client
|
|
3
|
+
* Tracks performance, cache efficiency, errors, and request statistics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface EndpointMetrics {
|
|
7
|
+
endpoint: string;
|
|
8
|
+
requests: {
|
|
9
|
+
total: number;
|
|
10
|
+
success: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
};
|
|
13
|
+
statusCodes: Record<number, number>;
|
|
14
|
+
timing: {
|
|
15
|
+
avgMs: number;
|
|
16
|
+
minMs: number;
|
|
17
|
+
maxMs: number;
|
|
18
|
+
p50Ms: number;
|
|
19
|
+
p95Ms: number;
|
|
20
|
+
p99Ms: number;
|
|
21
|
+
};
|
|
22
|
+
cache: {
|
|
23
|
+
l1Hits: number;
|
|
24
|
+
l1Misses: number;
|
|
25
|
+
l2Hits: number;
|
|
26
|
+
l2Misses: number;
|
|
27
|
+
hitRate: string; // e.g., "75.5%"
|
|
28
|
+
};
|
|
29
|
+
deduplication: {
|
|
30
|
+
hits: number;
|
|
31
|
+
misses: number;
|
|
32
|
+
hitRate: string;
|
|
33
|
+
};
|
|
34
|
+
bytes: {
|
|
35
|
+
sent: number;
|
|
36
|
+
received: number;
|
|
37
|
+
};
|
|
38
|
+
errors: Record<string, number>;
|
|
39
|
+
lastRequestAt: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GlobalMetrics {
|
|
43
|
+
totalRequests: number;
|
|
44
|
+
totalSuccess: number;
|
|
45
|
+
totalFailed: number;
|
|
46
|
+
avgResponseTimeMs: number;
|
|
47
|
+
totalBytesSent: number;
|
|
48
|
+
totalBytesReceived: number;
|
|
49
|
+
overallCacheHitRate: string;
|
|
50
|
+
overallDeduplicationRate: string;
|
|
51
|
+
endpoints: number;
|
|
52
|
+
uptime: number; // milliseconds since client creation
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MetricsAPI {
|
|
56
|
+
get(endpoint: string): EndpointMetrics | null;
|
|
57
|
+
getAll(): Record<string, EndpointMetrics>;
|
|
58
|
+
getGlobal(): GlobalMetrics;
|
|
59
|
+
reset(endpoint?: string): void;
|
|
60
|
+
export(): string; // JSON string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RequestMetric {
|
|
64
|
+
startTime: number;
|
|
65
|
+
responseTimeMs: number;
|
|
66
|
+
status: number;
|
|
67
|
+
success: boolean;
|
|
68
|
+
bytesSent: number;
|
|
69
|
+
bytesReceived: number;
|
|
70
|
+
cacheHit: boolean;
|
|
71
|
+
cacheLayer?: "l1" | "l2"; // Which cache layer hit
|
|
72
|
+
dedupeHit: boolean;
|
|
73
|
+
error?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class EndpointMetricsCollector {
|
|
77
|
+
private endpoint: string;
|
|
78
|
+
private requestHistory: RequestMetric[] = [];
|
|
79
|
+
private maxHistorySize = 10000; // Keep last 10k requests for percentile calculations
|
|
80
|
+
|
|
81
|
+
// Aggregated counters
|
|
82
|
+
private totalRequests = 0;
|
|
83
|
+
private successCount = 0;
|
|
84
|
+
private failedCount = 0;
|
|
85
|
+
private statusCodeCounts: Record<number, number> = {};
|
|
86
|
+
private l1CacheHits = 0;
|
|
87
|
+
private l1CacheMisses = 0;
|
|
88
|
+
private l2CacheHits = 0;
|
|
89
|
+
private l2CacheMisses = 0;
|
|
90
|
+
private dedupeHits = 0;
|
|
91
|
+
private dedupeMisses = 0;
|
|
92
|
+
private totalBytesSent = 0;
|
|
93
|
+
private totalBytesReceived = 0;
|
|
94
|
+
private errorCounts: Record<string, number> = {};
|
|
95
|
+
private lastRequestTimestamp: number | null = null;
|
|
96
|
+
|
|
97
|
+
// Response time tracking
|
|
98
|
+
private totalResponseTime = 0;
|
|
99
|
+
private minResponseTime = Infinity;
|
|
100
|
+
private maxResponseTime = 0;
|
|
101
|
+
|
|
102
|
+
constructor(endpoint: string) {
|
|
103
|
+
this.endpoint = endpoint;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Record a request metric
|
|
108
|
+
*/
|
|
109
|
+
recordRequest(metric: RequestMetric): void {
|
|
110
|
+
this.totalRequests++;
|
|
111
|
+
this.lastRequestTimestamp = Date.now();
|
|
112
|
+
|
|
113
|
+
// Success/failure
|
|
114
|
+
if (metric.success) {
|
|
115
|
+
this.successCount++;
|
|
116
|
+
} else {
|
|
117
|
+
this.failedCount++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Status codes
|
|
121
|
+
this.statusCodeCounts[metric.status] =
|
|
122
|
+
(this.statusCodeCounts[metric.status] || 0) + 1;
|
|
123
|
+
|
|
124
|
+
// Cache tracking
|
|
125
|
+
if (metric.cacheHit) {
|
|
126
|
+
if (metric.cacheLayer === "l1") {
|
|
127
|
+
this.l1CacheHits++;
|
|
128
|
+
} else if (metric.cacheLayer === "l2") {
|
|
129
|
+
this.l2CacheHits++;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Only count as miss if cache was checked (not deduped)
|
|
133
|
+
if (!metric.dedupeHit) {
|
|
134
|
+
this.l1CacheMisses++;
|
|
135
|
+
this.l2CacheMisses++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Deduplication
|
|
140
|
+
if (metric.dedupeHit) {
|
|
141
|
+
this.dedupeHits++;
|
|
142
|
+
} else {
|
|
143
|
+
this.dedupeMisses++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Bytes
|
|
147
|
+
this.totalBytesSent += metric.bytesSent;
|
|
148
|
+
this.totalBytesReceived += metric.bytesReceived;
|
|
149
|
+
|
|
150
|
+
// Errors
|
|
151
|
+
if (metric.error) {
|
|
152
|
+
this.errorCounts[metric.error] =
|
|
153
|
+
(this.errorCounts[metric.error] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Response time
|
|
157
|
+
this.totalResponseTime += metric.responseTimeMs;
|
|
158
|
+
this.minResponseTime = Math.min(
|
|
159
|
+
this.minResponseTime,
|
|
160
|
+
metric.responseTimeMs
|
|
161
|
+
);
|
|
162
|
+
this.maxResponseTime = Math.max(
|
|
163
|
+
this.maxResponseTime,
|
|
164
|
+
metric.responseTimeMs
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Store in history for percentile calculations
|
|
168
|
+
this.requestHistory.push(metric);
|
|
169
|
+
if (this.requestHistory.length > this.maxHistorySize) {
|
|
170
|
+
this.requestHistory.shift(); // Remove oldest
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Calculate percentiles from request history
|
|
176
|
+
*/
|
|
177
|
+
private calculatePercentile(percentile: number): number {
|
|
178
|
+
if (this.requestHistory.length === 0) return 0;
|
|
179
|
+
|
|
180
|
+
const sorted = this.requestHistory
|
|
181
|
+
.map((m) => m.responseTimeMs)
|
|
182
|
+
.sort((a, b) => a - b);
|
|
183
|
+
|
|
184
|
+
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
|
185
|
+
return sorted[Math.max(0, index)] || 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get current metrics snapshot
|
|
190
|
+
*/
|
|
191
|
+
getMetrics(): EndpointMetrics {
|
|
192
|
+
const totalCacheAttempts = this.l1CacheHits + this.l1CacheMisses;
|
|
193
|
+
const totalCacheHits = this.l1CacheHits + this.l2CacheHits;
|
|
194
|
+
const cacheHitRate =
|
|
195
|
+
totalCacheAttempts > 0
|
|
196
|
+
? ((totalCacheHits / totalCacheAttempts) * 100).toFixed(2) + "%"
|
|
197
|
+
: "0%";
|
|
198
|
+
|
|
199
|
+
const totalDedupeAttempts = this.dedupeHits + this.dedupeMisses;
|
|
200
|
+
const dedupeHitRate =
|
|
201
|
+
totalDedupeAttempts > 0
|
|
202
|
+
? ((this.dedupeHits / totalDedupeAttempts) * 100).toFixed(2) + "%"
|
|
203
|
+
: "0%";
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
endpoint: this.endpoint,
|
|
207
|
+
requests: {
|
|
208
|
+
total: this.totalRequests,
|
|
209
|
+
success: this.successCount,
|
|
210
|
+
failed: this.failedCount,
|
|
211
|
+
},
|
|
212
|
+
statusCodes: { ...this.statusCodeCounts },
|
|
213
|
+
timing: {
|
|
214
|
+
avgMs:
|
|
215
|
+
this.totalRequests > 0
|
|
216
|
+
? parseFloat(
|
|
217
|
+
(this.totalResponseTime / this.totalRequests).toFixed(2)
|
|
218
|
+
)
|
|
219
|
+
: 0,
|
|
220
|
+
minMs:
|
|
221
|
+
this.minResponseTime === Infinity
|
|
222
|
+
? 0
|
|
223
|
+
: parseFloat(this.minResponseTime.toFixed(2)),
|
|
224
|
+
maxMs: parseFloat(this.maxResponseTime.toFixed(2)),
|
|
225
|
+
p50Ms: parseFloat(this.calculatePercentile(50).toFixed(2)),
|
|
226
|
+
p95Ms: parseFloat(this.calculatePercentile(95).toFixed(2)),
|
|
227
|
+
p99Ms: parseFloat(this.calculatePercentile(99).toFixed(2)),
|
|
228
|
+
},
|
|
229
|
+
cache: {
|
|
230
|
+
l1Hits: this.l1CacheHits,
|
|
231
|
+
l1Misses: this.l1CacheMisses,
|
|
232
|
+
l2Hits: this.l2CacheHits,
|
|
233
|
+
l2Misses: this.l2CacheMisses,
|
|
234
|
+
hitRate: cacheHitRate,
|
|
235
|
+
},
|
|
236
|
+
deduplication: {
|
|
237
|
+
hits: this.dedupeHits,
|
|
238
|
+
misses: this.dedupeMisses,
|
|
239
|
+
hitRate: dedupeHitRate,
|
|
240
|
+
},
|
|
241
|
+
bytes: {
|
|
242
|
+
sent: this.totalBytesSent,
|
|
243
|
+
received: this.totalBytesReceived,
|
|
244
|
+
},
|
|
245
|
+
errors: { ...this.errorCounts },
|
|
246
|
+
lastRequestAt: this.lastRequestTimestamp,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Reset all metrics
|
|
252
|
+
*/
|
|
253
|
+
reset(): void {
|
|
254
|
+
this.requestHistory = [];
|
|
255
|
+
this.totalRequests = 0;
|
|
256
|
+
this.successCount = 0;
|
|
257
|
+
this.failedCount = 0;
|
|
258
|
+
this.statusCodeCounts = {};
|
|
259
|
+
this.l1CacheHits = 0;
|
|
260
|
+
this.l1CacheMisses = 0;
|
|
261
|
+
this.l2CacheHits = 0;
|
|
262
|
+
this.l2CacheMisses = 0;
|
|
263
|
+
this.dedupeHits = 0;
|
|
264
|
+
this.dedupeMisses = 0;
|
|
265
|
+
this.totalBytesSent = 0;
|
|
266
|
+
this.totalBytesReceived = 0;
|
|
267
|
+
this.errorCounts = {};
|
|
268
|
+
this.lastRequestTimestamp = null;
|
|
269
|
+
this.totalResponseTime = 0;
|
|
270
|
+
this.minResponseTime = Infinity;
|
|
271
|
+
this.maxResponseTime = 0;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Main Metrics Collector - manages metrics for all endpoints
|
|
277
|
+
*/
|
|
278
|
+
export class MetricsCollector implements MetricsAPI {
|
|
279
|
+
private endpoints: Map<string, EndpointMetricsCollector> = new Map();
|
|
280
|
+
private startTime: number;
|
|
281
|
+
|
|
282
|
+
constructor() {
|
|
283
|
+
this.startTime = Date.now();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get or create endpoint collector
|
|
288
|
+
*/
|
|
289
|
+
private getEndpointCollector(endpoint: string): EndpointMetricsCollector {
|
|
290
|
+
if (!this.endpoints.has(endpoint)) {
|
|
291
|
+
this.endpoints.set(endpoint, new EndpointMetricsCollector(endpoint));
|
|
292
|
+
}
|
|
293
|
+
return this.endpoints.get(endpoint)!;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Record a request event
|
|
298
|
+
*/
|
|
299
|
+
recordRequest(endpoint: string, metric: RequestMetric): void {
|
|
300
|
+
this.getEndpointCollector(endpoint).recordRequest(metric);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get metrics for a specific endpoint
|
|
305
|
+
*/
|
|
306
|
+
get(endpoint: string): EndpointMetrics | null {
|
|
307
|
+
const collector = this.endpoints.get(endpoint);
|
|
308
|
+
return collector ? collector.getMetrics() : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get metrics for all endpoints
|
|
313
|
+
*/
|
|
314
|
+
getAll(): Record<string, EndpointMetrics> {
|
|
315
|
+
const result: Record<string, EndpointMetrics> = {};
|
|
316
|
+
for (const [endpoint, collector] of this.endpoints) {
|
|
317
|
+
result[endpoint] = collector.getMetrics();
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get global aggregated metrics
|
|
324
|
+
*/
|
|
325
|
+
getGlobal(): GlobalMetrics {
|
|
326
|
+
let totalRequests = 0;
|
|
327
|
+
let totalSuccess = 0;
|
|
328
|
+
let totalFailed = 0;
|
|
329
|
+
let totalResponseTime = 0;
|
|
330
|
+
let totalBytesSent = 0;
|
|
331
|
+
let totalBytesReceived = 0;
|
|
332
|
+
let totalCacheHits = 0;
|
|
333
|
+
let totalCacheAttempts = 0;
|
|
334
|
+
let totalDedupeHits = 0;
|
|
335
|
+
let totalDedupeAttempts = 0;
|
|
336
|
+
|
|
337
|
+
for (const collector of this.endpoints.values()) {
|
|
338
|
+
const metrics = collector.getMetrics();
|
|
339
|
+
totalRequests += metrics.requests.total;
|
|
340
|
+
totalSuccess += metrics.requests.success;
|
|
341
|
+
totalFailed += metrics.requests.failed;
|
|
342
|
+
totalResponseTime += metrics.timing.avgMs * metrics.requests.total;
|
|
343
|
+
totalBytesSent += metrics.bytes.sent;
|
|
344
|
+
totalBytesReceived += metrics.bytes.received;
|
|
345
|
+
|
|
346
|
+
totalCacheHits += metrics.cache.l1Hits + metrics.cache.l2Hits;
|
|
347
|
+
totalCacheAttempts +=
|
|
348
|
+
metrics.cache.l1Hits +
|
|
349
|
+
metrics.cache.l1Misses +
|
|
350
|
+
metrics.cache.l2Hits +
|
|
351
|
+
metrics.cache.l2Misses;
|
|
352
|
+
|
|
353
|
+
totalDedupeHits += metrics.deduplication.hits;
|
|
354
|
+
totalDedupeAttempts +=
|
|
355
|
+
metrics.deduplication.hits + metrics.deduplication.misses;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const avgResponseTimeMs =
|
|
359
|
+
totalRequests > 0 ? totalResponseTime / totalRequests : 0;
|
|
360
|
+
|
|
361
|
+
const overallCacheHitRate =
|
|
362
|
+
totalCacheAttempts > 0
|
|
363
|
+
? ((totalCacheHits / totalCacheAttempts) * 100).toFixed(2) + "%"
|
|
364
|
+
: "0%";
|
|
365
|
+
|
|
366
|
+
const overallDeduplicationRate =
|
|
367
|
+
totalDedupeAttempts > 0
|
|
368
|
+
? ((totalDedupeHits / totalDedupeAttempts) * 100).toFixed(2) + "%"
|
|
369
|
+
: "0%";
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
totalRequests,
|
|
373
|
+
totalSuccess,
|
|
374
|
+
totalFailed,
|
|
375
|
+
avgResponseTimeMs: parseFloat(avgResponseTimeMs.toFixed(2)),
|
|
376
|
+
totalBytesSent,
|
|
377
|
+
totalBytesReceived,
|
|
378
|
+
overallCacheHitRate,
|
|
379
|
+
overallDeduplicationRate,
|
|
380
|
+
endpoints: this.endpoints.size,
|
|
381
|
+
uptime: Date.now() - this.startTime,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Reset metrics for specific endpoint or all
|
|
387
|
+
*/
|
|
388
|
+
reset(endpoint?: string): void {
|
|
389
|
+
if (endpoint) {
|
|
390
|
+
const collector = this.endpoints.get(endpoint);
|
|
391
|
+
if (collector) {
|
|
392
|
+
collector.reset();
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
// Reset all
|
|
396
|
+
for (const collector of this.endpoints.values()) {
|
|
397
|
+
collector.reset();
|
|
398
|
+
}
|
|
399
|
+
this.startTime = Date.now();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Export all metrics as JSON string
|
|
405
|
+
*/
|
|
406
|
+
export(): string {
|
|
407
|
+
return JSON.stringify(
|
|
408
|
+
{
|
|
409
|
+
global: this.getGlobal(),
|
|
410
|
+
endpoints: this.getAll(),
|
|
411
|
+
exportedAt: new Date().toISOString(),
|
|
412
|
+
},
|
|
413
|
+
null,
|
|
414
|
+
2
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Export for use in client.ts
|
|
420
|
+
export type { RequestMetric };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { toArrayBuffer, type Pointer } from "bun:ffi";
|
|
2
|
+
import { lib } from "./native";
|
|
3
|
+
import type { JirenResponse, JirenResponseBody } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Native Zig Cache wrapper
|
|
7
|
+
* Uses native HashMap for L1 (~0.001ms) and gzip disk storage for L2 (~2-5ms)
|
|
8
|
+
*/
|
|
9
|
+
export class NativeCache {
|
|
10
|
+
private ptr: Pointer | null;
|
|
11
|
+
|
|
12
|
+
constructor(l1Capacity = 100) {
|
|
13
|
+
this.ptr = lib.symbols.zcache_new(l1Capacity) as Pointer;
|
|
14
|
+
if (!this.ptr) throw new Error("Failed to create native cache");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get cached response by key
|
|
19
|
+
*/
|
|
20
|
+
get(url: string, path?: string, options?: any): JirenResponse | null {
|
|
21
|
+
if (!this.ptr) return null;
|
|
22
|
+
|
|
23
|
+
const key = this.generateKey(url, path, options);
|
|
24
|
+
const keyBuffer = Buffer.from(key + "\0");
|
|
25
|
+
|
|
26
|
+
const entryPtr = lib.symbols.zcache_get(this.ptr, keyBuffer) as Pointer;
|
|
27
|
+
if (!entryPtr) return null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Read ZCacheEntry struct:
|
|
31
|
+
// The struct layout in memory (with alignment):
|
|
32
|
+
// u16 status (offset 0, 2 bytes)
|
|
33
|
+
// [6 bytes padding]
|
|
34
|
+
// ptr headers_ptr (offset 8, 8 bytes)
|
|
35
|
+
// usize headers_len (offset 16, 8 bytes)
|
|
36
|
+
// ptr body_ptr (offset 24, 8 bytes)
|
|
37
|
+
// usize body_len (offset 32, 8 bytes)
|
|
38
|
+
|
|
39
|
+
// Create a view of the entry struct
|
|
40
|
+
const entryBytes = toArrayBuffer(entryPtr, 0, 40);
|
|
41
|
+
const entryView = new DataView(entryBytes);
|
|
42
|
+
const status = entryView.getUint16(0, true);
|
|
43
|
+
|
|
44
|
+
// Read pointer values as numbers (Bun FFI specific)
|
|
45
|
+
const headersLen = Number(entryView.getBigUint64(16, true));
|
|
46
|
+
const bodyLen = Number(entryView.getBigUint64(32, true));
|
|
47
|
+
|
|
48
|
+
// For now, create minimal headers and body
|
|
49
|
+
// The data is stored in native memory - reading pointers across FFI is complex
|
|
50
|
+
// We'll use the cached lengths to construct placeholder data
|
|
51
|
+
|
|
52
|
+
// Create an empty but valid response
|
|
53
|
+
const headers: Record<string, string> = {};
|
|
54
|
+
|
|
55
|
+
// Create body with empty data for now
|
|
56
|
+
// In a full implementation, we'd need to properly read from native pointers
|
|
57
|
+
const bodyBuffer = new ArrayBuffer(0);
|
|
58
|
+
|
|
59
|
+
// Reconstruct body methods
|
|
60
|
+
const body: JirenResponseBody = {
|
|
61
|
+
bodyUsed: false,
|
|
62
|
+
text: async () => "",
|
|
63
|
+
json: async <R>() => ({} as R),
|
|
64
|
+
arrayBuffer: async () => bodyBuffer,
|
|
65
|
+
blob: async () => new Blob([bodyBuffer]),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
status,
|
|
70
|
+
statusText: status === 200 ? "OK" : String(status),
|
|
71
|
+
headers,
|
|
72
|
+
url,
|
|
73
|
+
ok: status >= 200 && status < 300,
|
|
74
|
+
redirected: false,
|
|
75
|
+
type: "default",
|
|
76
|
+
body,
|
|
77
|
+
};
|
|
78
|
+
} finally {
|
|
79
|
+
lib.symbols.zcache_entry_free(entryPtr);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set response in cache
|
|
85
|
+
*/
|
|
86
|
+
set(
|
|
87
|
+
url: string,
|
|
88
|
+
response: JirenResponse,
|
|
89
|
+
ttl: number,
|
|
90
|
+
path?: string,
|
|
91
|
+
options?: any
|
|
92
|
+
): void {
|
|
93
|
+
if (!this.ptr) return;
|
|
94
|
+
|
|
95
|
+
const key = this.generateKey(url, path, options);
|
|
96
|
+
const keyBuffer = Buffer.from(key + "\0");
|
|
97
|
+
|
|
98
|
+
// Get body and headers as buffers
|
|
99
|
+
// We need to read body synchronously, so we'll need the original data
|
|
100
|
+
// For cached responses, we should store the raw data
|
|
101
|
+
// This is a simplified version - in production, handle async properly
|
|
102
|
+
|
|
103
|
+
// For now, skip if body is not immediately available
|
|
104
|
+
// The proper solution would be to call this after body is consumed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set response with raw data (called after body is available)
|
|
109
|
+
*/
|
|
110
|
+
async setWithData(
|
|
111
|
+
url: string,
|
|
112
|
+
status: number,
|
|
113
|
+
headers: string,
|
|
114
|
+
body: string,
|
|
115
|
+
ttl: number,
|
|
116
|
+
path?: string,
|
|
117
|
+
options?: any
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
if (!this.ptr) return;
|
|
120
|
+
|
|
121
|
+
const key = this.generateKey(url, path, options);
|
|
122
|
+
const keyBuffer = Buffer.from(key + "\0");
|
|
123
|
+
|
|
124
|
+
const headersBuffer = Buffer.from(headers);
|
|
125
|
+
const bodyBuffer = Buffer.from(body);
|
|
126
|
+
|
|
127
|
+
lib.symbols.zcache_set(
|
|
128
|
+
this.ptr,
|
|
129
|
+
keyBuffer,
|
|
130
|
+
status,
|
|
131
|
+
headersBuffer,
|
|
132
|
+
headersBuffer.length,
|
|
133
|
+
bodyBuffer,
|
|
134
|
+
bodyBuffer.length,
|
|
135
|
+
ttl
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Preload L2 disk cache into L1 memory
|
|
141
|
+
*/
|
|
142
|
+
preloadL1(url: string, path?: string, options?: any): boolean {
|
|
143
|
+
if (!this.ptr) return false;
|
|
144
|
+
|
|
145
|
+
const key = this.generateKey(url, path, options);
|
|
146
|
+
const keyBuffer = Buffer.from(key + "\0");
|
|
147
|
+
|
|
148
|
+
return lib.symbols.zcache_preload_l1(
|
|
149
|
+
this.ptr,
|
|
150
|
+
keyBuffer
|
|
151
|
+
) as unknown as boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clear all cache
|
|
156
|
+
*/
|
|
157
|
+
clear(url?: string): void {
|
|
158
|
+
if (!this.ptr) return;
|
|
159
|
+
lib.symbols.zcache_clear(this.ptr);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Free native resources
|
|
164
|
+
*/
|
|
165
|
+
close(): void {
|
|
166
|
+
if (this.ptr) {
|
|
167
|
+
lib.symbols.zcache_free(this.ptr);
|
|
168
|
+
this.ptr = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate cache key from URL and options
|
|
174
|
+
*/
|
|
175
|
+
private generateKey(url: string, path?: string, options?: any): string {
|
|
176
|
+
const fullUrl = path ? `${url}${path}` : url;
|
|
177
|
+
const method = options?.method || "GET";
|
|
178
|
+
const headers = JSON.stringify(options?.headers || {});
|
|
179
|
+
return `${method}:${fullUrl}:${headers}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -85,6 +85,32 @@ export const symbols = {
|
|
|
85
85
|
"void *",
|
|
86
86
|
"bool",
|
|
87
87
|
]),
|
|
88
|
+
|
|
89
|
+
// =========================================================================
|
|
90
|
+
// CACHE FFI
|
|
91
|
+
// =========================================================================
|
|
92
|
+
|
|
93
|
+
zcache_new: lib.func("zcache_new", "void *", ["uint64_t"]),
|
|
94
|
+
zcache_free: lib.func("zcache_free", "void", ["void *"]),
|
|
95
|
+
zcache_get: lib.func("zcache_get", "void *", ["void *", "const char *"]),
|
|
96
|
+
zcache_entry_free: lib.func("zcache_entry_free", "void", ["void *"]),
|
|
97
|
+
zcache_set: lib.func("zcache_set", "void", [
|
|
98
|
+
"void *", // cache
|
|
99
|
+
"const char *", // key
|
|
100
|
+
"uint16_t", // status
|
|
101
|
+
"void *", // headers_ptr
|
|
102
|
+
"uint64_t", // headers_len
|
|
103
|
+
"void *", // body_ptr
|
|
104
|
+
"uint64_t", // body_len
|
|
105
|
+
"int64_t", // ttl
|
|
106
|
+
]),
|
|
107
|
+
zcache_preload_l1: lib.func("zcache_preload_l1", "bool", [
|
|
108
|
+
"void *",
|
|
109
|
+
"const char *",
|
|
110
|
+
]),
|
|
111
|
+
zcache_clear: lib.func("zcache_clear", "void", ["void *"]),
|
|
112
|
+
zcache_stats: lib.func("zcache_stats", "void *", ["void *"]),
|
|
113
|
+
zcache_stats_free: lib.func("zcache_stats_free", "void", ["void *"]),
|
|
88
114
|
};
|
|
89
115
|
|
|
90
116
|
// Export a wrapper that matches structure of bun:ffi lib
|