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.
- package/README.md +1 -9
- package/components/client-node-native.ts +150 -292
- package/components/client.ts +385 -286
- package/components/index.ts +0 -2
- package/components/native-cache-node.ts +0 -3
- package/components/native-cache.ts +0 -8
- package/components/native-node.ts +142 -4
- package/components/native.ts +33 -65
- package/components/types.ts +25 -41
- package/dist/components/client-node-native.d.ts +0 -1
- package/dist/components/client-node-native.d.ts.map +1 -1
- package/dist/components/client-node-native.js +84 -202
- package/dist/components/client-node-native.js.map +1 -1
- package/dist/components/native-cache-node.d.ts.map +1 -1
- package/dist/components/native-cache-node.js +0 -1
- package/dist/components/native-cache-node.js.map +1 -1
- package/dist/components/native-node.d.ts +78 -0
- package/dist/components/native-node.d.ts.map +1 -1
- package/dist/components/native-node.js +125 -2
- package/dist/components/native-node.js.map +1 -1
- package/dist/components/types.d.ts +5 -13
- package/dist/components/types.d.ts.map +1 -1
- package/lib/libhttpclient.dylib +0 -0
- package/package.json +3 -3
- package/components/cache.ts +0 -451
- package/components/native-json.ts +0 -195
- package/components/persistent-worker.ts +0 -67
- package/components/subprocess-worker.ts +0 -60
- package/components/worker-pool.ts +0 -153
- package/components/worker.ts +0 -154
- package/dist/components/cache.d.ts +0 -32
- package/dist/components/cache.d.ts.map +0 -1
- package/dist/components/cache.js +0 -374
- package/dist/components/cache.js.map +0 -1
package/components/cache.ts
DELETED
|
@@ -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" }));
|