jiren 3.1.0 → 3.3.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.
Files changed (34) hide show
  1. package/README.md +1 -9
  2. package/components/client-node-native.ts +150 -292
  3. package/components/client.ts +385 -286
  4. package/components/index.ts +0 -2
  5. package/components/native-cache-node.ts +0 -3
  6. package/components/native-cache.ts +0 -8
  7. package/components/native-node.ts +142 -4
  8. package/components/native.ts +33 -65
  9. package/components/types.ts +25 -41
  10. package/dist/components/client-node-native.d.ts +0 -1
  11. package/dist/components/client-node-native.d.ts.map +1 -1
  12. package/dist/components/client-node-native.js +84 -202
  13. package/dist/components/client-node-native.js.map +1 -1
  14. package/dist/components/native-cache-node.d.ts.map +1 -1
  15. package/dist/components/native-cache-node.js +0 -1
  16. package/dist/components/native-cache-node.js.map +1 -1
  17. package/dist/components/native-node.d.ts +78 -0
  18. package/dist/components/native-node.d.ts.map +1 -1
  19. package/dist/components/native-node.js +125 -2
  20. package/dist/components/native-node.js.map +1 -1
  21. package/dist/components/types.d.ts +5 -13
  22. package/dist/components/types.d.ts.map +1 -1
  23. package/lib/libhttpclient.dylib +0 -0
  24. package/package.json +3 -3
  25. package/components/cache.ts +0 -451
  26. package/components/native-json.ts +0 -195
  27. package/components/persistent-worker.ts +0 -67
  28. package/components/subprocess-worker.ts +0 -60
  29. package/components/worker-pool.ts +0 -153
  30. package/components/worker.ts +0 -154
  31. package/dist/components/cache.d.ts +0 -32
  32. package/dist/components/cache.d.ts.map +0 -1
  33. package/dist/components/cache.js +0 -374
  34. package/dist/components/cache.js.map +0 -1
@@ -1,451 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- writeFileSync,
6
- unlinkSync,
7
- readdirSync,
8
- statSync,
9
- } from "fs";
10
- import { gzipSync, gunzipSync } from "zlib";
11
- import { createHash } from "crypto";
12
- import { join } from "path";
13
- import type {
14
- JirenResponse,
15
- JirenResponseBody,
16
- SerializableCacheEntry,
17
- CacheEntry,
18
- } from "./types.js";
19
-
20
- function reconstructBody(bodyData: string, isText: boolean): JirenResponseBody {
21
- let buffer: ArrayBuffer;
22
-
23
- if (isText) {
24
- // Text data stored directly
25
- const encoder = new TextEncoder();
26
- buffer = encoder.encode(bodyData).buffer as ArrayBuffer;
27
- } else {
28
- // Binary data stored as base64
29
- const binaryString = atob(bodyData);
30
- const bytes = new Uint8Array(binaryString.length);
31
- for (let i = 0; i < binaryString.length; i++) {
32
- bytes[i] = binaryString.charCodeAt(i);
33
- }
34
- buffer = bytes.buffer as ArrayBuffer;
35
- }
36
-
37
- return {
38
- bodyUsed: false,
39
- text: async () => {
40
- if (isText) return bodyData;
41
- const decoder = new TextDecoder();
42
- return decoder.decode(buffer);
43
- },
44
- json: async () => {
45
- const text = isText ? bodyData : new TextDecoder().decode(buffer);
46
- return JSON.parse(text);
47
- },
48
- arrayBuffer: async () => buffer,
49
- jsonFields: async <T extends Record<string, any> = any>(
50
- _fields: (keyof T)[]
51
- ) => ({}),
52
- blob: async () => new Blob([buffer]),
53
- };
54
- }
55
-
56
- interface LRUNode {
57
- key: string;
58
- entry: CacheEntry;
59
- prev: LRUNode | null;
60
- next: LRUNode | null;
61
- }
62
-
63
- class L1MemoryCache {
64
- private capacity: number;
65
- private cache: Map<string, LRUNode> = new Map();
66
- private head: LRUNode | null = null; // Most recently used
67
- private tail: LRUNode | null = null; // Least recently used
68
-
69
- // Stats
70
- public hits = 0;
71
- public misses = 0;
72
-
73
- constructor(capacity = 100) {
74
- this.capacity = capacity;
75
- }
76
-
77
- get(key: string): CacheEntry | null {
78
- const node = this.cache.get(key);
79
- if (!node) {
80
- this.misses++;
81
- return null;
82
- }
83
-
84
- // Check if expired
85
- const now = Date.now();
86
- if (now - node.entry.timestamp > node.entry.ttl) {
87
- this.delete(key);
88
- this.misses++;
89
- return null;
90
- }
91
-
92
- // Move to front (most recently used)
93
- this.moveToFront(node);
94
- this.hits++;
95
- return node.entry;
96
- }
97
-
98
- set(key: string, entry: CacheEntry): void {
99
- // If key exists, update and move to front
100
- const existing = this.cache.get(key);
101
- if (existing) {
102
- existing.entry = entry;
103
- this.moveToFront(existing);
104
- return;
105
- }
106
-
107
- // Create new node
108
- const node: LRUNode = {
109
- key,
110
- entry,
111
- prev: null,
112
- next: this.head,
113
- };
114
-
115
- // Add to front
116
- if (this.head) {
117
- this.head.prev = node;
118
- }
119
- this.head = node;
120
- if (!this.tail) {
121
- this.tail = node;
122
- }
123
-
124
- this.cache.set(key, node);
125
-
126
- // Evict LRU if over capacity
127
- if (this.cache.size > this.capacity) {
128
- this.evictLRU();
129
- }
130
- }
131
-
132
- delete(key: string): void {
133
- const node = this.cache.get(key);
134
- if (!node) return;
135
-
136
- this.removeNode(node);
137
- this.cache.delete(key);
138
- }
139
-
140
- clear(): void {
141
- this.cache.clear();
142
- this.head = null;
143
- this.tail = null;
144
- }
145
-
146
- stats() {
147
- return {
148
- size: this.cache.size,
149
- capacity: this.capacity,
150
- hits: this.hits,
151
- misses: this.misses,
152
- hitRate:
153
- this.hits + this.misses > 0
154
- ? ((this.hits / (this.hits + this.misses)) * 100).toFixed(2) + "%"
155
- : "0%",
156
- };
157
- }
158
-
159
- private moveToFront(node: LRUNode): void {
160
- if (node === this.head) return; // Already at front
161
-
162
- this.removeNode(node);
163
-
164
- // Add to front
165
- node.prev = null;
166
- node.next = this.head;
167
- if (this.head) {
168
- this.head.prev = node;
169
- }
170
- this.head = node;
171
- if (!this.tail) {
172
- this.tail = node;
173
- }
174
- }
175
-
176
- private removeNode(node: LRUNode): void {
177
- if (node.prev) {
178
- node.prev.next = node.next;
179
- } else {
180
- this.head = node.next;
181
- }
182
-
183
- if (node.next) {
184
- node.next.prev = node.prev;
185
- } else {
186
- this.tail = node.prev;
187
- }
188
- }
189
-
190
- private evictLRU(): void {
191
- if (!this.tail) return;
192
-
193
- const key = this.tail.key;
194
- this.removeNode(this.tail);
195
- this.cache.delete(key);
196
- }
197
- }
198
-
199
- export class ResponseCache {
200
- private l1: L1MemoryCache;
201
- private cacheDir: string;
202
- private maxDiskSize: number;
203
-
204
- constructor(l1Capacity = 100, cacheDir = ".cache/jiren", maxDiskSize = 100) {
205
- this.l1 = new L1MemoryCache(l1Capacity);
206
- this.maxDiskSize = maxDiskSize;
207
- this.cacheDir = cacheDir;
208
-
209
- // Create cache directory if it doesn't exist
210
- if (!existsSync(this.cacheDir)) {
211
- mkdirSync(this.cacheDir, { recursive: true });
212
- }
213
- }
214
-
215
- private generateKey(url: string, path?: string, options?: any): string {
216
- const fullUrl = path ? `${url}${path}` : url;
217
- const method = options?.method || "GET";
218
- const headers = JSON.stringify(options?.headers || {});
219
- const key = `${method}:${fullUrl}:${headers}`;
220
-
221
- // Hash the key to create a valid filename
222
- return createHash("md5").update(key).digest("hex");
223
- }
224
-
225
- private getCacheFilePath(key: string): string {
226
- return join(this.cacheDir, `${key}.json.gz`);
227
- }
228
-
229
- preloadL1(url: string, path?: string, options?: any): boolean {
230
- const key = this.generateKey(url, path, options);
231
-
232
- // Check if already in L1
233
- if (this.l1.get(key)) {
234
- return true;
235
- }
236
-
237
- // Try to load from L2 disk
238
- const l2Entry = this.getFromDisk(key);
239
- if (l2Entry) {
240
- this.l1.set(key, l2Entry);
241
- return true;
242
- }
243
-
244
- return false;
245
- }
246
-
247
- get(url: string, path?: string, options?: any): JirenResponse | null {
248
- const key = this.generateKey(url, path, options);
249
-
250
- // L1: Check in-memory cache first (~0.001ms)
251
- const l1Entry = this.l1.get(key);
252
- if (l1Entry) {
253
- return l1Entry.response;
254
- }
255
-
256
- // L2: Check disk cache (~5ms)
257
- const l2Entry = this.getFromDisk(key);
258
- if (l2Entry) {
259
- // Promote to L1 for faster future access
260
- this.l1.set(key, l2Entry);
261
- return l2Entry.response;
262
- }
263
-
264
- return null;
265
- }
266
-
267
- private getFromDisk(key: string): CacheEntry | null {
268
- const filePath = this.getCacheFilePath(key);
269
-
270
- if (!existsSync(filePath)) return null;
271
-
272
- try {
273
- // Read compressed file
274
- const compressed = readFileSync(filePath);
275
-
276
- // Decompress
277
- const decompressed = gunzipSync(compressed);
278
- const data = decompressed.toString("utf-8");
279
- const serialized: SerializableCacheEntry = JSON.parse(data);
280
-
281
- // Check if expired
282
- const now = Date.now();
283
- if (now - serialized.timestamp > serialized.ttl) {
284
- // Delete expired cache file
285
- try {
286
- unlinkSync(filePath);
287
- } catch {}
288
- return null;
289
- }
290
-
291
- // Reconstruct the response with working body methods
292
- const response: JirenResponse = {
293
- status: serialized.status,
294
- statusText: serialized.statusText,
295
- headers: serialized.headers,
296
- url: serialized.url,
297
- ok: serialized.ok,
298
- redirected: serialized.redirected,
299
- type: serialized.type || "default",
300
- body: reconstructBody(serialized.bodyData, serialized.bodyIsText),
301
- };
302
-
303
- return {
304
- response,
305
- timestamp: serialized.timestamp,
306
- ttl: serialized.ttl,
307
- };
308
- } catch (error) {
309
- // Invalid cache file, delete it
310
- try {
311
- unlinkSync(filePath);
312
- } catch {}
313
- return null;
314
- }
315
- }
316
-
317
- set(
318
- url: string,
319
- response: JirenResponse,
320
- ttl: number,
321
- path?: string,
322
- options?: any
323
- ): void {
324
- const key = this.generateKey(url, path, options);
325
-
326
- const entry: CacheEntry = {
327
- response,
328
- timestamp: Date.now(),
329
- ttl,
330
- };
331
-
332
- // L1: Store in memory (~0.001ms)
333
- this.l1.set(key, entry);
334
-
335
- // L2: Store on disk (async-ish, for persistence)
336
- this.setToDisk(key, entry);
337
- }
338
-
339
- private async setToDisk(key: string, entry: CacheEntry): Promise<void> {
340
- const filePath = this.getCacheFilePath(key);
341
-
342
- try {
343
- // Extract body text for serialization
344
- let bodyData: string;
345
- let bodyIsText = true;
346
-
347
- try {
348
- bodyData = await entry.response.body.text();
349
- } catch {
350
- // If text fails, try arrayBuffer and convert to base64
351
- try {
352
- const buffer = await entry.response.body.arrayBuffer();
353
- const bytes = new Uint8Array(buffer);
354
- let binary = "";
355
- for (let i = 0; i < bytes.length; i++) {
356
- binary += String.fromCharCode(bytes[i]!);
357
- }
358
- bodyData = btoa(binary);
359
- bodyIsText = false;
360
- } catch {
361
- bodyData = "";
362
- }
363
- }
364
-
365
- // Create serializable entry
366
- const serialized: SerializableCacheEntry = {
367
- status: entry.response.status,
368
- statusText: entry.response.statusText,
369
- headers: entry.response.headers as Record<string, string>,
370
- url: entry.response.url,
371
- ok: entry.response.ok,
372
- redirected: entry.response.redirected,
373
- type: entry.response.type || "default",
374
- bodyData,
375
- bodyIsText,
376
- timestamp: entry.timestamp,
377
- ttl: entry.ttl,
378
- };
379
-
380
- // Convert to JSON
381
- const json = JSON.stringify(serialized);
382
-
383
- // Compress with gzip
384
- const compressed = gzipSync(json);
385
-
386
- // Write compressed file
387
- writeFileSync(filePath, compressed);
388
- } catch (error) {
389
- // Silently fail if can't write cache
390
- console.warn("Failed to write cache:", error);
391
- }
392
- }
393
-
394
- clear(url?: string): void {
395
- // Clear L1
396
- this.l1.clear();
397
-
398
- // Clear L2
399
- if (url) {
400
- // Clear all cache files for this URL
401
- // This is approximate since we hash the keys
402
- // For now, just clear all to be safe
403
- this.clearAllDisk();
404
- } else {
405
- this.clearAllDisk();
406
- }
407
- }
408
-
409
- private clearAllDisk(): void {
410
- try {
411
- const files = readdirSync(this.cacheDir);
412
- for (const file of files) {
413
- if (file.endsWith(".json.gz")) {
414
- unlinkSync(join(this.cacheDir, file));
415
- }
416
- }
417
- } catch (error) {
418
- // Silently fail
419
- }
420
- }
421
-
422
- stats() {
423
- const l1Stats = this.l1.stats();
424
-
425
- // L2 disk stats
426
- let diskSize = 0;
427
- let diskFiles = 0;
428
- let totalDiskBytes = 0;
429
-
430
- try {
431
- const files = readdirSync(this.cacheDir);
432
- const cacheFiles = files.filter((f: string) => f.endsWith(".json.gz"));
433
- diskFiles = cacheFiles.length;
434
-
435
- for (const file of cacheFiles) {
436
- const stats = statSync(join(this.cacheDir, file));
437
- totalDiskBytes += stats.size;
438
- }
439
- } catch {}
440
-
441
- return {
442
- l1: l1Stats,
443
- l2: {
444
- size: diskFiles,
445
- maxSize: this.maxDiskSize,
446
- cacheDir: this.cacheDir,
447
- totalSizeKB: (totalDiskBytes / 1024).toFixed(2),
448
- },
449
- };
450
- }
451
- }
@@ -1,195 +0,0 @@
1
- /**
2
- * Native JSON Parser
3
- *
4
- * Uses native Zig std.json for parsing large JSON payloads.
5
- * For small payloads (<10KB), JavaScript JSON.parse is faster due to FFI overhead.
6
- * For large payloads (>10KB), native parsing is 2-3x faster.
7
- */
8
-
9
- import { lib } from "./native";
10
- import { toArrayBuffer } from "bun:ffi";
11
-
12
- type Pointer = number | null;
13
-
14
- // Threshold in bytes above which native JSON parsing is faster
15
- const NATIVE_JSON_THRESHOLD = 10240; // 10KB
16
-
17
- /**
18
- * Parse JSON with automatic native/JS fallback
19
- * Uses native Zig parser for large payloads, JS for small ones
20
- *
21
- * Note: For full object conversion, JS JSON.parse is still used.
22
- * Native parsing benefits are:
23
- * 1. validateJson() - Fast validation without parsing
24
- * 2. NativeJsonHandle - Extract specific fields without full parse
25
- * 3. parseJsonFields() - Get only needed fields from large JSON
26
- */
27
- export function parseJson<T = any>(data: string | Buffer): T {
28
- const buffer = typeof data === "string" ? Buffer.from(data) : data;
29
- // For full object conversion, JS JSON.parse is most efficient
30
- return JSON.parse(buffer.toString("utf-8"));
31
- }
32
-
33
- /**
34
- * Force native JSON validation before parsing
35
- * Validates natively (fast) then parses with JS
36
- * Use when you want to ensure valid JSON before parsing
37
- */
38
- export function parseJsonSafe<T = any>(data: string | Buffer): T {
39
- const buffer = typeof data === "string" ? Buffer.from(data) : data;
40
-
41
- // Validate natively first
42
- const isValid = lib.symbols.zjson_validate(buffer, buffer.length);
43
- if (!isValid) {
44
- throw new SyntaxError("Invalid JSON");
45
- }
46
-
47
- // Parse with JS
48
- return JSON.parse(buffer.toString("utf-8"));
49
- }
50
-
51
- /**
52
- * Validate JSON without parsing (fast check)
53
- * ~2x faster than try { JSON.parse } catch
54
- */
55
- export function validateJson(data: string | Buffer): boolean {
56
- const buffer = typeof data === "string" ? Buffer.from(data) : data;
57
- return lib.symbols.zjson_validate(buffer, buffer.length) as boolean;
58
- }
59
-
60
- /**
61
- * Native JSON handle for accessing parsed data without JS conversion
62
- * Useful for extracting specific fields from large JSON without full parse
63
- */
64
- export class NativeJsonHandle {
65
- private ptr: Pointer;
66
-
67
- constructor(data: string | Buffer) {
68
- const buffer = typeof data === "string" ? Buffer.from(data) : data;
69
- this.ptr = lib.symbols.zjson_parse(buffer, buffer.length) as Pointer;
70
-
71
- if (!this.ptr) {
72
- throw new SyntaxError("Failed to parse JSON");
73
- }
74
- }
75
-
76
- /**
77
- * Get string value by key
78
- */
79
- getString(key: string): string | null {
80
- if (!this.ptr) return null;
81
-
82
- const keyBuf = Buffer.from(key + "\0");
83
- const strPtr = lib.symbols.zjson_get_string(
84
- this.ptr as any,
85
- keyBuf
86
- ) as Pointer;
87
-
88
- if (!strPtr) return null;
89
-
90
- const len = Number(
91
- lib.symbols.zjson_get_string_len(this.ptr as any, keyBuf)
92
- );
93
- if (len === 0) return "";
94
-
95
- return Buffer.from(toArrayBuffer(strPtr as any, 0, len)).toString("utf-8");
96
- }
97
-
98
- /**
99
- * Get integer value by key
100
- */
101
- getInt(key: string): number {
102
- if (!this.ptr) return 0;
103
- const keyBuf = Buffer.from(key + "\0");
104
- return Number(lib.symbols.zjson_get_int(this.ptr as any, keyBuf));
105
- }
106
-
107
- /**
108
- * Get boolean value by key
109
- */
110
- getBool(key: string): boolean {
111
- if (!this.ptr) return false;
112
- const keyBuf = Buffer.from(key + "\0");
113
- return lib.symbols.zjson_get_bool(this.ptr as any, keyBuf) as boolean;
114
- }
115
-
116
- /**
117
- * Check if key exists
118
- */
119
- hasKey(key: string): boolean {
120
- if (!this.ptr) return false;
121
- const keyBuf = Buffer.from(key + "\0");
122
- return lib.symbols.zjson_has_key(this.ptr as any, keyBuf) as boolean;
123
- }
124
-
125
- /**
126
- * Get type (0=null, 1=bool, 2=int, 3=float, 4=string, 5=array, 6=object)
127
- */
128
- getType(): number {
129
- if (!this.ptr) return 0;
130
- return Number(lib.symbols.zjson_get_type(this.ptr as any));
131
- }
132
-
133
- /**
134
- * Get array length (if root is array)
135
- */
136
- getArrayLength(): number {
137
- if (!this.ptr) return 0;
138
- return Number(lib.symbols.zjson_array_len(this.ptr as any));
139
- }
140
-
141
- /**
142
- * Get object key count (if root is object)
143
- */
144
- getObjectLength(): number {
145
- if (!this.ptr) return 0;
146
- return Number(lib.symbols.zjson_object_len(this.ptr as any));
147
- }
148
-
149
- /**
150
- * Free native resources
151
- */
152
- close(): void {
153
- if (this.ptr) {
154
- lib.symbols.zjson_free(this.ptr as any);
155
- this.ptr = null;
156
- }
157
- }
158
- }
159
-
160
- /**
161
- * Parse JSON and get specific fields without full JS object conversion
162
- * More efficient for extracting a few fields from large JSON
163
- */
164
- export function parseJsonFields<T extends Record<string, any>>(
165
- data: string | Buffer,
166
- fields: (keyof T)[]
167
- ): Partial<T> {
168
- const handle = new NativeJsonHandle(data);
169
- const result: Partial<T> = {};
170
-
171
- try {
172
- for (const field of fields) {
173
- const key = String(field);
174
- if (handle.hasKey(key)) {
175
- // Try string first (most common)
176
- const strVal = handle.getString(key);
177
- if (strVal !== null) {
178
- (result as any)[field] = strVal;
179
- } else {
180
- // Try int
181
- const intVal = handle.getInt(key);
182
- if (intVal !== 0) {
183
- (result as any)[field] = intVal;
184
- } else {
185
- // Try bool
186
- (result as any)[field] = handle.getBool(key);
187
- }
188
- }
189
- }
190
- }
191
- return result;
192
- } finally {
193
- handle.close();
194
- }
195
- }
@@ -1,67 +0,0 @@
1
- import { CString, type Pointer } from "bun:ffi";
2
- import { lib } from "./native";
3
-
4
- const ptr: Pointer | null = lib.symbols.zclient_new();
5
- if (!ptr) {
6
- console.error("ERROR: Failed to create client");
7
- process.exit(1);
8
- }
9
-
10
- lib.symbols.zclient_set_benchmark_mode(ptr, true);
11
-
12
- const decoder = new TextDecoder();
13
-
14
- process.stdin.on("data", async (chunk: Buffer) => {
15
- const lines = decoder.decode(chunk).trim().split("\n");
16
-
17
- for (const line of lines) {
18
- if (!line) continue;
19
-
20
- try {
21
- const { id, url, method = "GET" } = JSON.parse(line);
22
-
23
- if (url === "PREFETCH") {
24
- console.log(JSON.stringify({ id, type: "prefetch_done" }));
25
- continue;
26
- }
27
-
28
- if (url === "QUIT") {
29
- lib.symbols.zclient_free(ptr);
30
- process.exit(0);
31
- }
32
-
33
- const methodBuffer = Buffer.from(method + "\0");
34
- const urlBuffer = Buffer.from(url + "\0");
35
-
36
- const respPtr = lib.symbols.zclient_request_full(
37
- ptr,
38
- methodBuffer,
39
- urlBuffer,
40
- null,
41
- null,
42
- 5,
43
- false
44
- );
45
-
46
- if (!respPtr) {
47
- console.log(
48
- JSON.stringify({ id, success: false, error: "Request failed" })
49
- );
50
- continue;
51
- }
52
-
53
- const status = lib.symbols.zfull_response_status(respPtr);
54
- const bodyLen = Number(lib.symbols.zfull_response_body_len(respPtr));
55
-
56
- lib.symbols.zclient_response_full_free(respPtr);
57
-
58
- console.log(JSON.stringify({ id, success: true, status, bodyLen }));
59
- } catch (err: any) {
60
- console.log(
61
- JSON.stringify({ id: 0, success: false, error: err.message })
62
- );
63
- }
64
- }
65
- });
66
-
67
- console.log(JSON.stringify({ type: "ready" }));