jiren 1.4.5 → 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.
@@ -26,6 +26,9 @@ export type {
26
26
  ErrorInterceptor,
27
27
  InterceptorRequestContext,
28
28
  InterceptorResponseContext,
29
+ EndpointMetrics,
30
+ GlobalMetrics,
31
+ MetricsAPI,
29
32
  } from "./types";
30
33
 
31
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 };
@@ -82,6 +82,48 @@ export const ffiDef = {
82
82
  returns: FFIType.void,
83
83
  },
84
84
 
85
+ // =========================================================================
86
+ // OPTIMIZED SINGLE-CALL API
87
+ // =========================================================================
88
+
89
+ zclient_request_full: {
90
+ args: [
91
+ FFIType.ptr, // client
92
+ FFIType.cstring, // method
93
+ FFIType.cstring, // url
94
+ FFIType.cstring, // headers (nullable)
95
+ FFIType.cstring, // body (nullable)
96
+ FFIType.u8, // max_redirects
97
+ FFIType.bool, // antibot
98
+ ],
99
+ returns: FFIType.ptr, // ZFullResponse*
100
+ },
101
+ zclient_response_full_free: {
102
+ args: [FFIType.ptr],
103
+ returns: FFIType.void,
104
+ },
105
+ // Accessor functions for ZFullResponse
106
+ zfull_response_status: {
107
+ args: [FFIType.ptr],
108
+ returns: FFIType.u16,
109
+ },
110
+ zfull_response_body: {
111
+ args: [FFIType.ptr],
112
+ returns: FFIType.ptr,
113
+ },
114
+ zfull_response_body_len: {
115
+ args: [FFIType.ptr],
116
+ returns: FFIType.u64,
117
+ },
118
+ zfull_response_headers: {
119
+ args: [FFIType.ptr],
120
+ returns: FFIType.ptr,
121
+ },
122
+ zfull_response_headers_len: {
123
+ args: [FFIType.ptr],
124
+ returns: FFIType.u64,
125
+ },
126
+
85
127
  // =========================================================================
86
128
  // CACHE FFI
87
129
  // =========================================================================
@@ -90,11 +90,11 @@ export interface ParsedUrl {
90
90
  path: string;
91
91
  }
92
92
 
93
- /** Warmup URL with a key for type-safe access */
94
- export interface WarmupUrlConfig {
93
+ /** Target URL with a key for type-safe access */
94
+ export interface TargetUrlConfig {
95
95
  /** Unique key to access this URL via client.url[key] */
96
96
  key: string;
97
- /** The URL to warmup */
97
+ /** The target URL */
98
98
  url: string;
99
99
  /** Enable response caching for this URL (default: false) */
100
100
  cache?: boolean | CacheConfig;
@@ -183,8 +183,8 @@ export interface UrlEndpoint {
183
183
  prefetch(options?: UrlRequestOptions): Promise<void>;
184
184
  }
185
185
 
186
- /** Type helper to extract keys from warmup config array */
187
- export type ExtractWarmupKeys<T extends readonly WarmupUrlConfig[]> =
186
+ /** Type helper to extract keys from target config array */
187
+ export type ExtractTargetKeys<T extends readonly TargetUrlConfig[]> =
188
188
  T[number]["key"];
189
189
 
190
190
  /** Context passed to request interceptors */
@@ -224,3 +224,63 @@ export interface Interceptors {
224
224
  response?: ResponseInterceptor[];
225
225
  error?: ErrorInterceptor[];
226
226
  }
227
+
228
+ /** Metrics for a single endpoint */
229
+ export interface EndpointMetrics {
230
+ endpoint: string;
231
+ requests: {
232
+ total: number;
233
+ success: number;
234
+ failed: number;
235
+ };
236
+ statusCodes: Record<number, number>;
237
+ timing: {
238
+ avgMs: number;
239
+ minMs: number;
240
+ maxMs: number;
241
+ p50Ms: number;
242
+ p95Ms: number;
243
+ p99Ms: number;
244
+ };
245
+ cache: {
246
+ l1Hits: number;
247
+ l1Misses: number;
248
+ l2Hits: number;
249
+ l2Misses: number;
250
+ hitRate: string;
251
+ };
252
+ deduplication: {
253
+ hits: number;
254
+ misses: number;
255
+ hitRate: string;
256
+ };
257
+ bytes: {
258
+ sent: number;
259
+ received: number;
260
+ };
261
+ errors: Record<string, number>;
262
+ lastRequestAt: number | null;
263
+ }
264
+
265
+ /** Global aggregated metrics across all endpoints */
266
+ export interface GlobalMetrics {
267
+ totalRequests: number;
268
+ totalSuccess: number;
269
+ totalFailed: number;
270
+ avgResponseTimeMs: number;
271
+ totalBytesSent: number;
272
+ totalBytesReceived: number;
273
+ overallCacheHitRate: string;
274
+ overallDeduplicationRate: string;
275
+ endpoints: number;
276
+ uptime: number;
277
+ }
278
+
279
+ /** Public API for accessing metrics */
280
+ export interface MetricsAPI {
281
+ get(endpoint: string): EndpointMetrics | null;
282
+ getAll(): Record<string, EndpointMetrics>;
283
+ getGlobal(): GlobalMetrics;
284
+ reset(endpoint?: string): void;
285
+ export(): string;
286
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jiren",
3
- "version": "1.4.5",
3
+ "version": "1.5.0",
4
4
  "author": "",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",