@ff-labs/fff-node 0.10.4-nightly.e2cad2f → 0.10.5-dev.774d6bc

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/dist/src/ffi.js DELETED
@@ -1,1365 +0,0 @@
1
- /**
2
- * Node.js FFI bindings for the fff-c native library using ffi-rs
3
- *
4
- * This module uses ffi-rs to call into the Rust C library.
5
- * All functions follow the Result pattern for error handling.
6
- *
7
- * The API is instance-based: `ffiCreate` returns an opaque handle that must
8
- * be passed to all subsequent calls and freed with `ffiDestroy`.
9
- *
10
- * ## Memory management
11
- *
12
- * Every `fff_*` function returning `*mut FffResult` allocates with Rust's Box.
13
- * We MUST call `fff_free_result` to properly deallocate (not libc::free).
14
- *
15
- * ## FffResult struct reading
16
- *
17
- * The FffResult struct layout (#[repr(C)]):
18
- * offset 0: success (bool, 1 byte + 7 padding)
19
- * offset 8: data pointer (8 bytes) - *mut c_char (JSON string or null)
20
- * offset 16: error pointer (8 bytes) - *mut c_char (error message or null)
21
- * offset 24: handle pointer (8 bytes) - *mut c_void (instance handle or null)
22
- *
23
- * ## Two-step approach for reading + freeing
24
- *
25
- * ffi-rs auto-dereferences struct retType pointers, losing the original pointer.
26
- * We solve this by:
27
- * 1. Calling the C function with `retType: DataType.External` to get the raw pointer
28
- * 2. Using `restorePointer` to read the struct fields from the raw pointer
29
- * 3. Calling `fff_free_result` with the original raw pointer
30
- *
31
- * ## Null pointer detection
32
- *
33
- * `isNullPointer` from ffi-rs correctly detects null C pointers wrapped as
34
- * V8 External objects. We use this instead of truthy checks.
35
- */
36
- import { close, createPointer, DataType, freePointer, funcConstructor, isNullPointer, load, open, PointerType, restorePointer, unwrapPointer, wrapPointer, } from "ffi-rs";
37
- import { findBinary } from "./binary.js";
38
- import { createGrepCursor, err } from "./fff-api.js";
39
- const LIBRARY_KEY = "fff_c";
40
- const FFF_CREATE_OPTIONS_STRUCT = {
41
- version: DataType.U32,
42
- base_path: DataType.String,
43
- frecency_db_path: DataType.String,
44
- history_db_path: DataType.String,
45
- enable_mmap_cache: DataType.U8,
46
- enable_content_indexing: DataType.U8,
47
- watch: DataType.U8,
48
- ai_mode: DataType.U8,
49
- log_file_path: DataType.String,
50
- log_level: DataType.String,
51
- cache_budget_max_files: DataType.U64,
52
- cache_budget_max_bytes: DataType.U64,
53
- cache_budget_max_file_size: DataType.U64,
54
- enable_fs_root_scanning: DataType.U8,
55
- enable_home_dir_scanning: DataType.U8,
56
- follow_symlinks: DataType.U8,
57
- };
58
- // ALWAYS KEEP IN SYNC WITH fff.h
59
- const FFF_CREATE_OPTIONS_VERSION = 2;
60
- /** Grep mode constants matching the C API (u8). */
61
- const GREP_MODE_PLAIN = 0;
62
- const GREP_MODE_REGEX = 1;
63
- const GREP_MODE_FUZZY = 2;
64
- /** Map string mode to u8 */
65
- function grepModeToU8(mode) {
66
- switch (mode) {
67
- case "regex":
68
- return GREP_MODE_REGEX;
69
- case "fuzzy":
70
- return GREP_MODE_FUZZY;
71
- default:
72
- return GREP_MODE_PLAIN;
73
- }
74
- }
75
- // Track whether the library is loaded
76
- let isLoaded = false;
77
- /**
78
- * Struct type definition for FffResult used with restorePointer.
79
- *
80
- * Uses U8 for the bool success field (correct alignment with ffi-rs).
81
- * Uses External for ALL pointer fields to avoid hangs on null char* pointers
82
- * (ffi-rs hangs when trying to read DataType.String from null char*).
83
- */
84
- const FFF_RESULT_STRUCT = {
85
- success: DataType.U8,
86
- error: DataType.External,
87
- handle: DataType.External,
88
- int_value: DataType.I64,
89
- };
90
- /**
91
- * Load the native library using ffi-rs
92
- */
93
- function loadLibrary() {
94
- if (isLoaded)
95
- return;
96
- const binaryPath = findBinary();
97
- if (!binaryPath) {
98
- throw new Error("fff native library not found. Run `npx @ff-labs/fff-node download` or build from source with `cargo build --release -p fff-c`");
99
- }
100
- open({ library: LIBRARY_KEY, path: binaryPath });
101
- isLoaded = true;
102
- }
103
- /**
104
- * Convert snake_case keys to camelCase recursively
105
- */
106
- function snakeToCamel(obj) {
107
- if (obj === null || obj === undefined)
108
- return obj;
109
- if (typeof obj !== "object")
110
- return obj;
111
- if (Array.isArray(obj))
112
- return obj.map(snakeToCamel);
113
- const result = {};
114
- for (const [key, value] of Object.entries(obj)) {
115
- const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
116
- result[camelKey] = snakeToCamel(value);
117
- }
118
- return result;
119
- }
120
- /**
121
- * Read a C string (char*) from an ffi-rs External pointer.
122
- *
123
- * Uses restorePointer + wrapPointer to dereference the char* and read the
124
- * null-terminated string. Returns null if the pointer is null.
125
- */
126
- function readCString(ptr) {
127
- if (isNullPointer(ptr))
128
- return null;
129
- try {
130
- const [str] = restorePointer({
131
- retType: [DataType.String],
132
- paramsValue: wrapPointer([ptr]),
133
- });
134
- return str;
135
- }
136
- catch {
137
- return null;
138
- }
139
- }
140
- /**
141
- * Call a C function that returns `*mut FffResult` and get both the raw pointer
142
- * (for freeing) and the parsed struct fields.
143
- *
144
- * Step 1: Call function with `DataType.External` retType → raw pointer
145
- * Step 2: Use `restorePointer` to read struct fields from the raw pointer
146
- */
147
- function callRaw(funcName, paramsType, paramsValue) {
148
- const rawPtr = load({
149
- library: LIBRARY_KEY,
150
- funcName,
151
- retType: DataType.External,
152
- paramsType,
153
- paramsValue,
154
- freeResultMemory: false,
155
- });
156
- const [structData] = restorePointer({
157
- retType: [FFF_RESULT_STRUCT],
158
- paramsValue: wrapPointer([rawPtr]),
159
- });
160
- return { rawPtr, struct: structData };
161
- }
162
- /**
163
- * Free a FffResult pointer by calling fff_free_result.
164
- *
165
- * This frees the FffResult struct and its data/error strings using Rust's
166
- * Box::from_raw and CString::from_raw. The handle field is NOT freed.
167
- */
168
- function freeResult(resultPtr) {
169
- try {
170
- load({
171
- library: LIBRARY_KEY,
172
- funcName: "fff_free_result",
173
- retType: DataType.Void,
174
- paramsType: [DataType.External],
175
- paramsValue: [resultPtr],
176
- });
177
- }
178
- catch {
179
- // Ignore cleanup errors
180
- }
181
- }
182
- /**
183
- * Read the FffResult envelope from a raw call. Returns the parsed struct + raw pointer.
184
- * On error, frees the result and returns a Result error.
185
- */
186
- function readResultEnvelope(funcName, paramsType, paramsValue) {
187
- loadLibrary();
188
- const { rawPtr, struct: structData } = callRaw(funcName, paramsType, paramsValue);
189
- if (structData.success === 0) {
190
- const errorStr = readCString(structData.error);
191
- freeResult(rawPtr);
192
- return err(errorStr || "Unknown error");
193
- }
194
- return { rawPtr, struct: structData };
195
- }
196
- /** Call a function returning FffResult with void payload. */
197
- function callVoidResult(funcName, paramsType, paramsValue) {
198
- const res = readResultEnvelope(funcName, paramsType, paramsValue);
199
- if ("ok" in res)
200
- return res;
201
- freeResult(res.rawPtr);
202
- return { ok: true, value: undefined };
203
- }
204
- /** Call a function returning FffResult with int_value payload. */
205
- function callIntResult(funcName, paramsType, paramsValue) {
206
- const res = readResultEnvelope(funcName, paramsType, paramsValue);
207
- if ("ok" in res)
208
- return res;
209
- const value = Number(res.struct.int_value);
210
- freeResult(res.rawPtr);
211
- return { ok: true, value };
212
- }
213
- /** Call a function returning FffResult with bool in int_value. */
214
- function callBoolResult(funcName, paramsType, paramsValue) {
215
- const res = readResultEnvelope(funcName, paramsType, paramsValue);
216
- if ("ok" in res)
217
- return res;
218
- const value = Number(res.struct.int_value) !== 0;
219
- freeResult(res.rawPtr);
220
- return { ok: true, value };
221
- }
222
- /** Call a function returning FffResult with a C string in handle. */
223
- function callStringResult(funcName, paramsType, paramsValue) {
224
- const res = readResultEnvelope(funcName, paramsType, paramsValue);
225
- if ("ok" in res)
226
- return res;
227
- const handlePtr = res.struct.handle;
228
- freeResult(res.rawPtr);
229
- if (isNullPointer(handlePtr))
230
- return { ok: true, value: null };
231
- const str = readCString(handlePtr);
232
- freeString(handlePtr);
233
- return { ok: true, value: str };
234
- }
235
- /** Call a function returning FffResult with a JSON string in handle. */
236
- function callJsonResult(funcName, paramsType, paramsValue) {
237
- const res = readResultEnvelope(funcName, paramsType, paramsValue);
238
- if ("ok" in res)
239
- return res;
240
- const handlePtr = res.struct.handle;
241
- freeResult(res.rawPtr);
242
- if (isNullPointer(handlePtr))
243
- return { ok: true, value: undefined };
244
- const jsonStr = readCString(handlePtr);
245
- freeString(handlePtr);
246
- if (jsonStr === null || jsonStr === "")
247
- return { ok: true, value: undefined };
248
- try {
249
- return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) };
250
- }
251
- catch {
252
- return { ok: true, value: jsonStr };
253
- }
254
- }
255
- /** Free a C string via fff_free_string. */
256
- function freeString(ptr) {
257
- try {
258
- load({
259
- library: LIBRARY_KEY,
260
- funcName: "fff_free_string",
261
- retType: DataType.Void,
262
- paramsType: [DataType.External],
263
- paramsValue: [ptr],
264
- });
265
- }
266
- catch {
267
- // Ignore
268
- }
269
- }
270
- export function ffiCreate(basePath, frecencyDbPath, historyDbPath, _useUnsafeNoLock, enableMmapCache, enableContentIndexing, watch, aiMode, logFilePath, logLevel, cacheBudgetMaxFiles, cacheBudgetMaxBytes, cacheBudgetMaxFileSize, enableFsRootScanning, enableHomeDirScanning, followSymlinks) {
271
- loadLibrary();
272
- const optsValue = {
273
- version: FFF_CREATE_OPTIONS_VERSION,
274
- base_path: basePath,
275
- frecency_db_path: frecencyDbPath,
276
- history_db_path: historyDbPath,
277
- enable_mmap_cache: enableMmapCache ? 1 : 0,
278
- enable_content_indexing: enableContentIndexing ? 1 : 0,
279
- watch: watch ? 1 : 0,
280
- ai_mode: aiMode ? 1 : 0,
281
- log_file_path: logFilePath,
282
- log_level: logLevel,
283
- cache_budget_max_files: cacheBudgetMaxFiles,
284
- cache_budget_max_bytes: cacheBudgetMaxBytes,
285
- cache_budget_max_file_size: cacheBudgetMaxFileSize,
286
- enable_fs_root_scanning: enableFsRootScanning ? 1 : 0,
287
- enable_home_dir_scanning: enableHomeDirScanning ? 1 : 0,
288
- follow_symlinks: followSymlinks ? 1 : 0,
289
- };
290
- const rawPtr = load({
291
- library: LIBRARY_KEY,
292
- funcName: "fff_create_instance_with",
293
- retType: DataType.External,
294
- paramsType: [FFF_CREATE_OPTIONS_STRUCT],
295
- paramsValue: [optsValue],
296
- freeResultMemory: false,
297
- });
298
- const [structData] = restorePointer({
299
- retType: [FFF_RESULT_STRUCT],
300
- paramsValue: wrapPointer([rawPtr]),
301
- });
302
- const success = structData.success !== 0;
303
- try {
304
- if (success) {
305
- const handle = structData.handle;
306
- if (isNullPointer(handle)) {
307
- return err("fff_create_instance_with returned null handle");
308
- }
309
- return { ok: true, value: handle };
310
- }
311
- else {
312
- const errorStr = readCString(structData.error);
313
- return err(errorStr || "Unknown error");
314
- }
315
- }
316
- finally {
317
- freeResult(rawPtr);
318
- }
319
- }
320
- /**
321
- * Destroy and clean up an instance.
322
- */
323
- export function ffiDestroy(handle) {
324
- loadLibrary();
325
- load({
326
- library: LIBRARY_KEY,
327
- funcName: "fff_destroy",
328
- retType: DataType.Void,
329
- paramsType: [DataType.External],
330
- paramsValue: [handle],
331
- });
332
- }
333
- // ---------------------------------------------------------------------------
334
- // Struct type definitions for restorePointer (must match #[repr(C)] layout)
335
- // ---------------------------------------------------------------------------
336
- const FFF_FILE_ITEM_STRUCT = {
337
- relative_path: DataType.External,
338
- file_name: DataType.External,
339
- git_status: DataType.External,
340
- size: DataType.U64,
341
- modified: DataType.U64,
342
- access_frecency_score: DataType.I64,
343
- modification_frecency_score: DataType.I64,
344
- total_frecency_score: DataType.I64,
345
- is_binary: DataType.U8,
346
- };
347
- const FFF_SCORE_STRUCT = {
348
- total: DataType.I32,
349
- base_score: DataType.I32,
350
- filename_bonus: DataType.I32,
351
- special_filename_bonus: DataType.I32,
352
- frecency_boost: DataType.I32,
353
- distance_penalty: DataType.I32,
354
- current_file_penalty: DataType.I32,
355
- combo_match_boost: DataType.I32,
356
- exact_match: DataType.U8,
357
- match_type: DataType.External,
358
- };
359
- const FFF_SEARCH_RESULT_STRUCT = {
360
- items: DataType.External,
361
- scores: DataType.External,
362
- count: DataType.U32,
363
- total_matched: DataType.U32,
364
- total_files: DataType.U32,
365
- // FffLocation inlined (flattened)
366
- location_tag: DataType.U8,
367
- location_line: DataType.I32,
368
- location_col: DataType.I32,
369
- location_end_line: DataType.I32,
370
- location_end_col: DataType.I32,
371
- };
372
- // FffDirItem struct (#[repr(C)]): char* (8) + char* (8) + i32 (4) + 4 padding = 24 bytes
373
- const FFF_DIR_ITEM_STRUCT = {
374
- relative_path: DataType.External,
375
- dir_name: DataType.External,
376
- max_access_frecency: DataType.I32,
377
- };
378
- const FFF_DIR_SEARCH_RESULT_STRUCT = {
379
- items: DataType.External,
380
- scores: DataType.External,
381
- count: DataType.U32,
382
- total_matched: DataType.U32,
383
- total_dirs: DataType.U32,
384
- };
385
- // FffMixedItem struct (#[repr(C)]): u8 (1) + 7 padding + char* (8) + char* (8) + char* (8)
386
- // + u64 (8) + u64 (8) + i64 (8) + i64 (8) + i64 (8) + bool (1) + 7 padding = 80 bytes
387
- const FFF_MIXED_ITEM_STRUCT = {
388
- item_type: DataType.U8,
389
- relative_path: DataType.External,
390
- display_name: DataType.External,
391
- git_status: DataType.External,
392
- size: DataType.U64,
393
- modified: DataType.U64,
394
- access_frecency_score: DataType.I64,
395
- modification_frecency_score: DataType.I64,
396
- total_frecency_score: DataType.I64,
397
- is_binary: DataType.U8,
398
- };
399
- const FFF_MIXED_SEARCH_RESULT_STRUCT = {
400
- items: DataType.External,
401
- scores: DataType.External,
402
- count: DataType.U32,
403
- total_matched: DataType.U32,
404
- total_files: DataType.U32,
405
- total_dirs: DataType.U32,
406
- // FffLocation inlined (flattened)
407
- location_tag: DataType.U8,
408
- location_line: DataType.I32,
409
- location_col: DataType.I32,
410
- location_end_line: DataType.I32,
411
- location_end_col: DataType.I32,
412
- };
413
- const FFF_GREP_MATCH_STRUCT = {
414
- relative_path: DataType.External,
415
- file_name: DataType.External,
416
- git_status: DataType.External,
417
- line_content: DataType.External,
418
- match_ranges: DataType.External,
419
- context_before: DataType.External,
420
- context_after: DataType.External,
421
- size: DataType.U64,
422
- modified: DataType.U64,
423
- total_frecency_score: DataType.I64,
424
- access_frecency_score: DataType.I64,
425
- modification_frecency_score: DataType.I64,
426
- line_number: DataType.U64,
427
- byte_offset: DataType.U64,
428
- col: DataType.U32,
429
- match_ranges_count: DataType.U32,
430
- context_before_count: DataType.U32,
431
- context_after_count: DataType.U32,
432
- fuzzy_score: DataType.U32, // actually u16 in C, but ffi-rs doesn't so we read it as u32 with padding
433
- has_fuzzy_score: DataType.U8,
434
- is_binary: DataType.U8,
435
- is_definition: DataType.U8,
436
- };
437
- const FFF_GREP_RESULT_STRUCT = {
438
- items: DataType.External,
439
- count: DataType.U32,
440
- total_matched: DataType.U32,
441
- total_files_searched: DataType.U32,
442
- total_files: DataType.U32,
443
- filtered_file_count: DataType.U32,
444
- next_file_offset: DataType.U32,
445
- regex_fallback_error: DataType.External,
446
- };
447
- const FFF_MATCH_RANGE_STRUCT = {
448
- start: DataType.U32,
449
- end: DataType.U32,
450
- };
451
- // ---------------------------------------------------------------------------
452
- // Struct reading helpers
453
- // ---------------------------------------------------------------------------
454
- function readFileItemFromRaw(raw) {
455
- return {
456
- relativePath: readCString(raw.relative_path) ?? "",
457
- fileName: readCString(raw.file_name) ?? "",
458
- gitStatus: readCString(raw.git_status) ?? "",
459
- size: Number(raw.size),
460
- modified: Number(raw.modified),
461
- accessFrecencyScore: Number(raw.access_frecency_score),
462
- modificationFrecencyScore: Number(raw.modification_frecency_score),
463
- totalFrecencyScore: Number(raw.total_frecency_score),
464
- };
465
- }
466
- function readScoreFromRaw(raw) {
467
- return {
468
- total: raw.total,
469
- baseScore: raw.base_score,
470
- filenameBonus: raw.filename_bonus,
471
- specialFilenameBonus: raw.special_filename_bonus,
472
- frecencyBoost: raw.frecency_boost,
473
- distancePenalty: raw.distance_penalty,
474
- currentFilePenalty: raw.current_file_penalty,
475
- comboMatchBoost: raw.combo_match_boost,
476
- exactMatch: raw.exact_match !== 0,
477
- matchType: readCString(raw.match_type) ?? "",
478
- };
479
- }
480
- function readDirItemFromRaw(raw) {
481
- return {
482
- relativePath: readCString(raw.relative_path) ?? "",
483
- dirName: readCString(raw.dir_name) ?? "",
484
- maxAccessFrecency: raw.max_access_frecency,
485
- };
486
- }
487
- function readMixedItemFromRaw(raw) {
488
- if (raw.item_type === 1) {
489
- // Directory
490
- return {
491
- type: "directory",
492
- item: {
493
- relativePath: readCString(raw.relative_path) ?? "",
494
- dirName: readCString(raw.display_name) ?? "",
495
- maxAccessFrecency: Number(raw.access_frecency_score),
496
- },
497
- };
498
- }
499
- // File (item_type === 0)
500
- return {
501
- type: "file",
502
- item: {
503
- relativePath: readCString(raw.relative_path) ?? "",
504
- fileName: readCString(raw.display_name) ?? "",
505
- gitStatus: readCString(raw.git_status) ?? "",
506
- size: Number(raw.size),
507
- modified: Number(raw.modified),
508
- accessFrecencyScore: Number(raw.access_frecency_score),
509
- modificationFrecencyScore: Number(raw.modification_frecency_score),
510
- totalFrecencyScore: Number(raw.total_frecency_score),
511
- },
512
- };
513
- }
514
- /**
515
- * Call an accessor function that returns a pointer to a struct element,
516
- * then read the struct from that pointer.
517
- */
518
- function callAccessor(funcName, resultPtr, index, structDef) {
519
- loadLibrary();
520
- const elemPtr = load({
521
- library: LIBRARY_KEY,
522
- funcName,
523
- retType: DataType.External,
524
- paramsType: [DataType.External, DataType.U32],
525
- paramsValue: [resultPtr, index],
526
- });
527
- const [raw] = restorePointer({
528
- retType: [structDef],
529
- paramsValue: wrapPointer([elemPtr]),
530
- });
531
- return raw;
532
- }
533
- /**
534
- * Offset a pointer by `bytes` using the C API helper.
535
- */
536
- function ptrOffset(base, bytes) {
537
- return load({
538
- library: LIBRARY_KEY,
539
- funcName: "fff_ptr_offset",
540
- retType: DataType.External,
541
- paramsType: [DataType.External, DataType.U64],
542
- paramsValue: [base, bytes],
543
- });
544
- }
545
- /**
546
- * Read a C string array (char**) of `count` elements.
547
- */
548
- function readCStringArray(ptrArray, count) {
549
- if (count === 0 || isNullPointer(ptrArray))
550
- return [];
551
- const result = [];
552
- for (let i = 0; i < count; i++) {
553
- const elemPtr = ptrOffset(ptrArray, i * 8);
554
- const [charPtr] = restorePointer({
555
- retType: [DataType.External],
556
- paramsValue: [elemPtr],
557
- });
558
- result.push(readCString(charPtr) ?? "");
559
- }
560
- return result;
561
- }
562
- function readGrepMatchFromRaw(raw) {
563
- // Read match_ranges array via pointer offsets
564
- const matchRanges = [];
565
- for (let i = 0; i < raw.match_ranges_count; i++) {
566
- const rangePtr = ptrOffset(raw.match_ranges, i * 8); // FffMatchRange is 8 bytes
567
- const [rangeRaw] = restorePointer({
568
- retType: [FFF_MATCH_RANGE_STRUCT],
569
- paramsValue: wrapPointer([rangePtr]),
570
- });
571
- matchRanges.push([rangeRaw.start, rangeRaw.end]);
572
- }
573
- const match = {
574
- relativePath: readCString(raw.relative_path) ?? "",
575
- fileName: readCString(raw.file_name) ?? "",
576
- gitStatus: readCString(raw.git_status) ?? "",
577
- lineContent: readCString(raw.line_content) ?? "",
578
- size: Number(raw.size),
579
- modified: Number(raw.modified),
580
- totalFrecencyScore: Number(raw.total_frecency_score),
581
- accessFrecencyScore: Number(raw.access_frecency_score),
582
- modificationFrecencyScore: Number(raw.modification_frecency_score),
583
- isBinary: raw.is_binary !== 0,
584
- lineNumber: Number(raw.line_number),
585
- col: raw.col,
586
- byteOffset: Number(raw.byte_offset),
587
- matchRanges,
588
- };
589
- if (raw.has_fuzzy_score !== 0) {
590
- match.fuzzyScore = raw.fuzzy_score;
591
- }
592
- if (raw.context_before_count > 0) {
593
- match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count);
594
- }
595
- if (raw.context_after_count > 0) {
596
- match.contextAfter = readCStringArray(raw.context_after, raw.context_after_count);
597
- }
598
- if (raw.is_definition !== 0) {
599
- match.isDefinition = true;
600
- }
601
- return match;
602
- }
603
- /**
604
- * Parse an FffGrepResult from `FffResult.handle`, then free native memory.
605
- */
606
- function parseGrepResult(rawPtr) {
607
- loadLibrary();
608
- const [envelope] = restorePointer({
609
- retType: [FFF_RESULT_STRUCT],
610
- paramsValue: wrapPointer([rawPtr]),
611
- });
612
- const success = envelope.success !== 0;
613
- if (!success) {
614
- const errorMsg = readCString(envelope.error) || "Unknown error";
615
- freeResult(rawPtr);
616
- return err(errorMsg);
617
- }
618
- const handlePtr = envelope.handle;
619
- freeResult(rawPtr);
620
- if (isNullPointer(handlePtr)) {
621
- return err("grep returned null result");
622
- }
623
- const [gr] = restorePointer({
624
- retType: [FFF_GREP_RESULT_STRUCT],
625
- paramsValue: wrapPointer([handlePtr]),
626
- });
627
- const count = gr.count;
628
- const regexFallbackError = readCString(gr.regex_fallback_error) ?? undefined;
629
- const items = [];
630
- for (let i = 0; i < count; i++) {
631
- const rawMatch = callAccessor("fff_grep_result_get_match", handlePtr, i, FFF_GREP_MATCH_STRUCT);
632
- items.push(readGrepMatchFromRaw(rawMatch));
633
- }
634
- // Free native grep result
635
- load({
636
- library: LIBRARY_KEY,
637
- funcName: "fff_free_grep_result",
638
- retType: DataType.Void,
639
- paramsType: [DataType.External],
640
- paramsValue: [handlePtr],
641
- });
642
- const grepResult = {
643
- items,
644
- totalMatched: gr.total_matched,
645
- totalFilesSearched: gr.total_files_searched,
646
- totalFiles: gr.total_files,
647
- filteredFileCount: gr.filtered_file_count,
648
- nextCursor: gr.next_file_offset > 0 ? createGrepCursor(gr.next_file_offset) : null,
649
- };
650
- if (regexFallbackError) {
651
- grepResult.regexFallbackError = regexFallbackError;
652
- }
653
- return { ok: true, value: grepResult };
654
- }
655
- /**
656
- * Parse an FffSearchResult from `FffResult.handle`, then free native memory.
657
- */
658
- function parseSearchResult(rawPtr) {
659
- loadLibrary();
660
- // Read FffResult envelope
661
- const [envelope] = restorePointer({
662
- retType: [FFF_RESULT_STRUCT],
663
- paramsValue: wrapPointer([rawPtr]),
664
- });
665
- const success = envelope.success !== 0;
666
- if (!success) {
667
- const errorMsg = readCString(envelope.error) || "Unknown error";
668
- freeResult(rawPtr);
669
- return err(errorMsg);
670
- }
671
- const handlePtr = envelope.handle;
672
- // Free the FffResult envelope (does NOT free handle)
673
- freeResult(rawPtr);
674
- if (isNullPointer(handlePtr)) {
675
- return err("fff_search returned null search result");
676
- }
677
- // Read FffSearchResult struct
678
- const [sr] = restorePointer({
679
- retType: [FFF_SEARCH_RESULT_STRUCT],
680
- paramsValue: wrapPointer([handlePtr]),
681
- });
682
- const count = sr.count;
683
- // Read location
684
- let location;
685
- if (sr.location_tag === 1) {
686
- location = { type: "line", line: sr.location_line };
687
- }
688
- else if (sr.location_tag === 2) {
689
- location = {
690
- type: "position",
691
- line: sr.location_line,
692
- col: sr.location_col,
693
- };
694
- }
695
- else if (sr.location_tag === 3) {
696
- location = {
697
- type: "range",
698
- start: { line: sr.location_line, col: sr.location_col },
699
- end: { line: sr.location_end_line, col: sr.location_end_col },
700
- };
701
- }
702
- // Read items and scores via accessor functions
703
- const items = [];
704
- const scores = [];
705
- for (let i = 0; i < count; i++) {
706
- const rawItem = callAccessor("fff_search_result_get_item", handlePtr, i, FFF_FILE_ITEM_STRUCT);
707
- items.push(readFileItemFromRaw(rawItem));
708
- const rawScore = callAccessor("fff_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
709
- scores.push(readScoreFromRaw(rawScore));
710
- }
711
- // Free native search result
712
- load({
713
- library: LIBRARY_KEY,
714
- funcName: "fff_free_search_result",
715
- retType: DataType.Void,
716
- paramsType: [DataType.External],
717
- paramsValue: [handlePtr],
718
- });
719
- const result = {
720
- items,
721
- scores,
722
- totalMatched: sr.total_matched,
723
- totalFiles: sr.total_files,
724
- };
725
- if (location) {
726
- result.location = location;
727
- }
728
- return { ok: true, value: result };
729
- }
730
- /**
731
- * Parse an FffDirSearchResult from `FffResult.handle`, then free native memory.
732
- */
733
- function parseDirSearchResult(rawPtr) {
734
- loadLibrary();
735
- // Read FffResult envelope
736
- const [envelope] = restorePointer({
737
- retType: [FFF_RESULT_STRUCT],
738
- paramsValue: wrapPointer([rawPtr]),
739
- });
740
- const success = envelope.success !== 0;
741
- if (!success) {
742
- const errorMsg = readCString(envelope.error) || "Unknown error";
743
- freeResult(rawPtr);
744
- return err(errorMsg);
745
- }
746
- const handlePtr = envelope.handle;
747
- // Free the FffResult envelope (does NOT free handle)
748
- freeResult(rawPtr);
749
- if (isNullPointer(handlePtr)) {
750
- return err("fff_search_directories returned null search result");
751
- }
752
- // Read FffDirSearchResult struct
753
- const [sr] = restorePointer({
754
- retType: [FFF_DIR_SEARCH_RESULT_STRUCT],
755
- paramsValue: wrapPointer([handlePtr]),
756
- });
757
- const count = sr.count;
758
- // Read items and scores via accessor functions
759
- const items = [];
760
- const scores = [];
761
- for (let i = 0; i < count; i++) {
762
- const rawItem = callAccessor("fff_dir_search_result_get_item", handlePtr, i, FFF_DIR_ITEM_STRUCT);
763
- items.push(readDirItemFromRaw(rawItem));
764
- const rawScore = callAccessor("fff_dir_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
765
- scores.push(readScoreFromRaw(rawScore));
766
- }
767
- // Free native dir search result
768
- load({
769
- library: LIBRARY_KEY,
770
- funcName: "fff_free_dir_search_result",
771
- retType: DataType.Void,
772
- paramsType: [DataType.External],
773
- paramsValue: [handlePtr],
774
- });
775
- return {
776
- ok: true,
777
- value: {
778
- items,
779
- scores,
780
- totalMatched: sr.total_matched,
781
- totalDirs: sr.total_dirs,
782
- },
783
- };
784
- }
785
- /**
786
- * Parse an FffMixedSearchResult from `FffResult.handle`, then free native memory.
787
- */
788
- function parseMixedSearchResult(rawPtr) {
789
- loadLibrary();
790
- // Read FffResult envelope
791
- const [envelope] = restorePointer({
792
- retType: [FFF_RESULT_STRUCT],
793
- paramsValue: wrapPointer([rawPtr]),
794
- });
795
- const success = envelope.success !== 0;
796
- if (!success) {
797
- const errorMsg = readCString(envelope.error) || "Unknown error";
798
- freeResult(rawPtr);
799
- return err(errorMsg);
800
- }
801
- const handlePtr = envelope.handle;
802
- // Free the FffResult envelope (does NOT free handle)
803
- freeResult(rawPtr);
804
- if (isNullPointer(handlePtr)) {
805
- return err("fff_search_mixed returned null search result");
806
- }
807
- // Read FffMixedSearchResult struct
808
- const [sr] = restorePointer({
809
- retType: [FFF_MIXED_SEARCH_RESULT_STRUCT],
810
- paramsValue: wrapPointer([handlePtr]),
811
- });
812
- const count = sr.count;
813
- // Read location
814
- let location;
815
- if (sr.location_tag === 1) {
816
- location = { type: "line", line: sr.location_line };
817
- }
818
- else if (sr.location_tag === 2) {
819
- location = {
820
- type: "position",
821
- line: sr.location_line,
822
- col: sr.location_col,
823
- };
824
- }
825
- else if (sr.location_tag === 3) {
826
- location = {
827
- type: "range",
828
- start: { line: sr.location_line, col: sr.location_col },
829
- end: { line: sr.location_end_line, col: sr.location_end_col },
830
- };
831
- }
832
- // Read items and scores via accessor functions
833
- const items = [];
834
- const scores = [];
835
- for (let i = 0; i < count; i++) {
836
- const rawItem = callAccessor("fff_mixed_search_result_get_item", handlePtr, i, FFF_MIXED_ITEM_STRUCT);
837
- items.push(readMixedItemFromRaw(rawItem));
838
- const rawScore = callAccessor("fff_mixed_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
839
- scores.push(readScoreFromRaw(rawScore));
840
- }
841
- // Free native mixed search result
842
- load({
843
- library: LIBRARY_KEY,
844
- funcName: "fff_free_mixed_search_result",
845
- retType: DataType.Void,
846
- paramsType: [DataType.External],
847
- paramsValue: [handlePtr],
848
- });
849
- const result = {
850
- items,
851
- scores,
852
- totalMatched: sr.total_matched,
853
- totalFiles: sr.total_files,
854
- totalDirs: sr.total_dirs,
855
- };
856
- if (location) {
857
- result.location = location;
858
- }
859
- return { ok: true, value: result };
860
- }
861
- /**
862
- * Perform fuzzy search.
863
- */
864
- export function ffiSearch(handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount) {
865
- loadLibrary();
866
- const rawPtr = load({
867
- library: LIBRARY_KEY,
868
- funcName: "fff_search",
869
- retType: DataType.External,
870
- paramsType: [
871
- DataType.External, // handle
872
- DataType.String, // query
873
- DataType.String, // current_file
874
- DataType.U32, // max_threads
875
- DataType.U32, // page_index
876
- DataType.U32, // page_size
877
- DataType.I32, // combo_boost_multiplier
878
- DataType.U32, // min_combo_count
879
- ],
880
- paramsValue: [
881
- handle,
882
- query,
883
- currentFile,
884
- maxThreads,
885
- pageIndex,
886
- pageSize,
887
- comboBoostMultiplier,
888
- minComboCount,
889
- ],
890
- freeResultMemory: false,
891
- });
892
- return parseSearchResult(rawPtr);
893
- }
894
- /**
895
- * Glob-only search. Bypasses the regular query parser, applies the pattern
896
- * as a single `Constraint::Glob`, ranks by frecency, paginates.
897
- */
898
- export function ffiGlob(handle, pattern, currentFile, maxThreads, pageIndex, pageSize) {
899
- loadLibrary();
900
- const rawPtr = load({
901
- library: LIBRARY_KEY,
902
- funcName: "fff_glob",
903
- retType: DataType.External,
904
- paramsType: [
905
- DataType.External, // handle
906
- DataType.String, // pattern
907
- DataType.String, // current_file
908
- DataType.U32, // max_threads
909
- DataType.U32, // page_index
910
- DataType.U32, // page_size
911
- ],
912
- paramsValue: [handle, pattern, currentFile, maxThreads, pageIndex, pageSize],
913
- freeResultMemory: false,
914
- });
915
- return parseSearchResult(rawPtr);
916
- }
917
- /**
918
- * Perform fuzzy directory search.
919
- */
920
- export function ffiSearchDirectories(handle, query, currentFile, maxThreads, pageIndex, pageSize) {
921
- loadLibrary();
922
- const rawPtr = load({
923
- library: LIBRARY_KEY,
924
- funcName: "fff_search_directories",
925
- retType: DataType.External,
926
- paramsType: [
927
- DataType.External, // handle
928
- DataType.String, // query
929
- DataType.String, // current_file
930
- DataType.U32, // max_threads
931
- DataType.U32, // page_index
932
- DataType.U32, // page_size
933
- ],
934
- paramsValue: [handle, query, currentFile ?? "", maxThreads, pageIndex, pageSize],
935
- freeResultMemory: false,
936
- });
937
- return parseDirSearchResult(rawPtr);
938
- }
939
- /**
940
- * Perform mixed (files + directories) fuzzy search.
941
- */
942
- export function ffiSearchMixed(handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount) {
943
- loadLibrary();
944
- const rawPtr = load({
945
- library: LIBRARY_KEY,
946
- funcName: "fff_search_mixed",
947
- retType: DataType.External,
948
- paramsType: [
949
- DataType.External, // handle
950
- DataType.String, // query
951
- DataType.String, // current_file
952
- DataType.U32, // max_threads
953
- DataType.U32, // page_index
954
- DataType.U32, // page_size
955
- DataType.I32, // combo_boost_multiplier
956
- DataType.U32, // min_combo_count
957
- ],
958
- paramsValue: [
959
- handle,
960
- query,
961
- currentFile,
962
- maxThreads,
963
- pageIndex,
964
- pageSize,
965
- comboBoostMultiplier,
966
- minComboCount,
967
- ],
968
- freeResultMemory: false,
969
- });
970
- return parseMixedSearchResult(rawPtr);
971
- }
972
- /**
973
- * Live grep - search file contents.
974
- */
975
- export function ffiLiveGrep(handle, query, mode, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
976
- loadLibrary();
977
- const rawPtr = load({
978
- library: LIBRARY_KEY,
979
- funcName: "fff_live_grep",
980
- retType: DataType.External,
981
- paramsType: [
982
- DataType.External, // handle
983
- DataType.String, // query
984
- DataType.U8, // mode
985
- DataType.U64, // max_file_size
986
- DataType.U32, // max_matches_per_file
987
- DataType.Boolean, // smart_case
988
- DataType.U32, // file_offset
989
- DataType.U32, // page_limit
990
- DataType.U64, // time_budget_ms
991
- DataType.U32, // before_context
992
- DataType.U32, // after_context
993
- DataType.Boolean, // classify_definitions
994
- ],
995
- paramsValue: [
996
- handle,
997
- query,
998
- grepModeToU8(mode),
999
- maxFileSize,
1000
- maxMatchesPerFile,
1001
- smartCase,
1002
- fileOffset,
1003
- pageLimit,
1004
- timeBudgetMs,
1005
- beforeContext,
1006
- afterContext,
1007
- classifyDefinitions,
1008
- ],
1009
- freeResultMemory: false,
1010
- });
1011
- return parseGrepResult(rawPtr);
1012
- }
1013
- /**
1014
- * Multi-pattern grep - Aho-Corasick multi-needle search.
1015
- */
1016
- export function ffiMultiGrep(handle, patternsJoined, constraints, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
1017
- loadLibrary();
1018
- const rawPtr = load({
1019
- library: LIBRARY_KEY,
1020
- funcName: "fff_multi_grep",
1021
- retType: DataType.External,
1022
- paramsType: [
1023
- DataType.External, // handle
1024
- DataType.String, // patterns_joined
1025
- DataType.String, // constraints
1026
- DataType.U64, // max_file_size
1027
- DataType.U32, // max_matches_per_file
1028
- DataType.Boolean, // smart_case
1029
- DataType.U32, // file_offset
1030
- DataType.U32, // page_limit
1031
- DataType.U64, // time_budget_ms
1032
- DataType.U32, // before_context
1033
- DataType.U32, // after_context
1034
- DataType.Boolean, // classify_definitions
1035
- ],
1036
- paramsValue: [
1037
- handle,
1038
- patternsJoined,
1039
- constraints,
1040
- maxFileSize,
1041
- maxMatchesPerFile,
1042
- smartCase,
1043
- fileOffset,
1044
- pageLimit,
1045
- timeBudgetMs,
1046
- beforeContext,
1047
- afterContext,
1048
- classifyDefinitions,
1049
- ],
1050
- freeResultMemory: false,
1051
- });
1052
- return parseGrepResult(rawPtr);
1053
- }
1054
- /**
1055
- * Trigger file scan.
1056
- */
1057
- export function ffiScanFiles(handle) {
1058
- return callVoidResult("fff_scan_files", [DataType.External], [handle]);
1059
- }
1060
- /**
1061
- * Check if scanning.
1062
- */
1063
- export function ffiIsScanning(handle) {
1064
- loadLibrary();
1065
- return load({
1066
- library: LIBRARY_KEY,
1067
- funcName: "fff_is_scanning",
1068
- retType: DataType.Boolean,
1069
- paramsType: [DataType.External],
1070
- paramsValue: [handle],
1071
- });
1072
- }
1073
- /**
1074
- * Get the base path of the file picker.
1075
- */
1076
- export function ffiGetBasePath(handle) {
1077
- return callStringResult("fff_get_base_path", [DataType.External], [handle]);
1078
- }
1079
- // FffScanProgress struct definition
1080
- const FFF_SCAN_PROGRESS_STRUCT = {
1081
- scanned_files_count: DataType.U64,
1082
- is_scanning: DataType.U8,
1083
- is_watcher_ready: DataType.U8,
1084
- is_warmup_complete: DataType.U8,
1085
- };
1086
- /**
1087
- * Get scan progress.
1088
- */
1089
- export function ffiGetScanProgress(handle) {
1090
- loadLibrary();
1091
- const res = readResultEnvelope("fff_get_scan_progress", [DataType.External], [handle]);
1092
- if ("ok" in res)
1093
- return res;
1094
- const handlePtr = res.struct.handle;
1095
- freeResult(res.rawPtr);
1096
- if (isNullPointer(handlePtr))
1097
- return err("scan progress returned null");
1098
- const [sp] = restorePointer({
1099
- retType: [FFF_SCAN_PROGRESS_STRUCT],
1100
- paramsValue: wrapPointer([handlePtr]),
1101
- });
1102
- const result = {
1103
- scannedFilesCount: Number(sp.scanned_files_count),
1104
- isScanning: sp.is_scanning !== 0,
1105
- isWatcherReady: sp.is_watcher_ready !== 0,
1106
- isWarmupComplete: sp.is_warmup_complete !== 0,
1107
- };
1108
- // Free native scan progress
1109
- load({
1110
- library: LIBRARY_KEY,
1111
- funcName: "fff_free_scan_progress",
1112
- retType: DataType.Void,
1113
- paramsType: [DataType.External],
1114
- paramsValue: [handlePtr],
1115
- });
1116
- return { ok: true, value: result };
1117
- }
1118
- /**
1119
- * Wait for a tree scan to complete.
1120
- */
1121
- export function ffiWaitForScan(handle, timeoutMs) {
1122
- return callBoolResult("fff_wait_for_scan", [DataType.External, DataType.U64], [handle, timeoutMs]);
1123
- }
1124
- /**
1125
- * Restart index in new path.
1126
- */
1127
- export function ffiRestartIndex(handle, newPath) {
1128
- return callVoidResult("fff_restart_index", [DataType.External, DataType.String], [handle, newPath]);
1129
- }
1130
- /**
1131
- * Refresh git status.
1132
- */
1133
- export function ffiRefreshGitStatus(handle) {
1134
- return callIntResult("fff_refresh_git_status", [DataType.External], [handle]);
1135
- }
1136
- /**
1137
- * Track query completion.
1138
- */
1139
- export function ffiTrackQuery(handle, query, filePath) {
1140
- return callBoolResult("fff_track_query", [DataType.External, DataType.String, DataType.String], [handle, query, filePath]);
1141
- }
1142
- /**
1143
- * Get historical query.
1144
- */
1145
- export function ffiGetHistoricalQuery(handle, offset) {
1146
- return callStringResult("fff_get_historical_query", [DataType.External, DataType.U64], [handle, offset]);
1147
- }
1148
- // ALWAYS KEEP IN SYNC WITH fff.h
1149
- //
1150
- // Note: node uses `fff_watch_args` (flattened options) because ffi-rs cannot
1151
- // marshal a `*const *const c_char` field inside a struct param — StringArray
1152
- // is only supported as a top-level parameter.
1153
- //
1154
- // Batch contents are read through the C accessors (fff_watch_events_count /
1155
- // fff_watch_events_get_path / fff_watch_events_get_kind), so no struct
1156
- // layout knowledge lives on this side.
1157
- /** Map the C kind byte to the public WatchEventKind. */
1158
- function watchKindFromU8(kind) {
1159
- switch (kind) {
1160
- case 0:
1161
- return "created";
1162
- case 1:
1163
- return "modified";
1164
- case 2:
1165
- return "removed";
1166
- default:
1167
- return "rescan";
1168
- }
1169
- }
1170
- /** Trampoline signature: (watch id, batch address, user_data — unused). */
1171
- const WATCH_TRAMPOLINE_TYPE = funcConstructor({
1172
- paramsType: [DataType.U64, DataType.U64, DataType.U64],
1173
- retType: DataType.Void,
1174
- });
1175
- /** JS handlers keyed by process-unique native watch id. */
1176
- const watchHandlers = new Map();
1177
- /** Instances (by handle identity) that ever created a watch subscription. */
1178
- const watchInstances = new Set();
1179
- /** Lazily created process-wide trampoline (createPointer result). */
1180
- let watchTrampoline = null;
1181
- /** Convert a raw u64 address delivered through the trampoline to a JsExternal. */
1182
- function addressToExternal(address) {
1183
- return load({
1184
- library: LIBRARY_KEY,
1185
- funcName: "fff_ptr_offset",
1186
- retType: DataType.External,
1187
- paramsType: [DataType.U64, DataType.U64],
1188
- paramsValue: [address, 0],
1189
- });
1190
- }
1191
- /** Parse an FffWatchEventBatch at `address` and free the native memory. */
1192
- function consumeWatchBatch(address) {
1193
- const batchPtr = addressToExternal(address);
1194
- const count = load({
1195
- library: LIBRARY_KEY,
1196
- funcName: "fff_watch_events_count",
1197
- retType: DataType.U32,
1198
- paramsType: [DataType.External],
1199
- paramsValue: [batchPtr],
1200
- });
1201
- const events = [];
1202
- for (let i = 0; i < count; i++) {
1203
- const path = load({
1204
- library: LIBRARY_KEY,
1205
- funcName: "fff_watch_events_get_path",
1206
- retType: DataType.External,
1207
- paramsType: [DataType.External, DataType.U32],
1208
- paramsValue: [batchPtr, i],
1209
- });
1210
- const kind = load({
1211
- library: LIBRARY_KEY,
1212
- funcName: "fff_watch_events_get_kind",
1213
- retType: DataType.U8,
1214
- paramsType: [DataType.External, DataType.U32],
1215
- paramsValue: [batchPtr, i],
1216
- });
1217
- events.push({
1218
- path: readCString(path) ?? "",
1219
- kind: watchKindFromU8(kind),
1220
- });
1221
- }
1222
- load({
1223
- library: LIBRARY_KEY,
1224
- funcName: "fff_free_watch_events",
1225
- retType: DataType.Void,
1226
- paramsType: [DataType.U64],
1227
- paramsValue: [address],
1228
- });
1229
- return events;
1230
- }
1231
- /**
1232
- * The single native->JS entry point for all watch subscriptions. Runs on
1233
- * the JS thread (threadsafe_function delivery); the batch is owned by us
1234
- * and freed inside `consumeWatchBatch`. Unknown watch ids (unsubscribe
1235
- * races) are benign: the batch is freed and dropped.
1236
- */
1237
- function watchTrampolineImpl(watchId, batchAddress, _userData) {
1238
- const events = consumeWatchBatch(batchAddress);
1239
- const handler = watchHandlers.get(Number(watchId));
1240
- if (handler === undefined || events.length === 0)
1241
- return;
1242
- try {
1243
- handler(events);
1244
- }
1245
- catch {
1246
- // User callback errors must not propagate into the FFI layer
1247
- }
1248
- }
1249
- function ensureWatchTrampoline() {
1250
- if (watchTrampoline === null) {
1251
- watchTrampoline = createPointer({
1252
- paramsType: [WATCH_TRAMPOLINE_TYPE],
1253
- paramsValue: [watchTrampolineImpl],
1254
- });
1255
- }
1256
- return unwrapPointer(watchTrampoline)[0];
1257
- }
1258
- // fff watcher uses a single cross-boundary FFI callback to deliver all events which we then manually
1259
- // mapping to the user's javascript functions
1260
- function ensureWatchCallbackRegistered(handle) {
1261
- if (watchInstances.has(handle))
1262
- return { ok: true, value: undefined };
1263
- const trampoline = ensureWatchTrampoline();
1264
- const registered = callVoidResult("fff_set_watch_callback", [DataType.External, DataType.External, DataType.U64], [handle, trampoline, 0]);
1265
- if (registered.ok)
1266
- watchInstances.add(handle);
1267
- return registered;
1268
- }
1269
- function releaseWatchTrampolineIfIdle() {
1270
- if (watchHandlers.size > 0 || watchInstances.size > 0 || watchTrampoline === null)
1271
- return;
1272
- freePointer({
1273
- paramsType: [WATCH_TRAMPOLINE_TYPE],
1274
- paramsValue: watchTrampoline,
1275
- pointerType: PointerType.RsPointer,
1276
- });
1277
- watchTrampoline = null;
1278
- }
1279
- /**
1280
- * Create a push-mode watch subscription. `callback` receives a normalized
1281
- * batch of up to 128 events, delivered on the JS event loop.
1282
- *
1283
- * Returns the native watch id to pass to `ffiUnwatch`.
1284
- */
1285
- export function ffiWatch(handle, pattern, ignore, callback) {
1286
- loadLibrary();
1287
- const registered = ensureWatchCallbackRegistered(handle);
1288
- if (!registered.ok)
1289
- return registered;
1290
- const created = callIntResult("fff_watch_args", [DataType.External, DataType.String, DataType.StringArray, DataType.U32], [handle, pattern, ignore, ignore.length]);
1291
- if (!created.ok)
1292
- return created;
1293
- // No startup race: threadsafe delivery lands on the JS event loop, so this
1294
- // synchronous set always precedes the first routing lookup for this id.
1295
- watchHandlers.set(created.value, callback);
1296
- return created;
1297
- }
1298
- /**
1299
- * Remove a watch subscription. Drops the JS handler synchronously — once
1300
- * this returns the callback can never run again (a late native tail batch
1301
- * misses the map lookup and is dropped).
1302
- */
1303
- export function ffiUnwatch(handle, watchId) {
1304
- const result = callBoolResult("fff_unwatch", [DataType.External, DataType.U64], [handle, watchId]);
1305
- watchHandlers.delete(watchId);
1306
- return result;
1307
- }
1308
- /**
1309
- * Post-`ffiDestroy` cleanup for an instance's watch state: drops any
1310
- * handlers that were never explicitly unwatched and releases the process
1311
- * trampoline when this was the last watching instance.
1312
- */
1313
- export function ffiWatchCleanupAfterDestroy(handle, watchIds) {
1314
- for (const id of watchIds) {
1315
- watchHandlers.delete(id);
1316
- }
1317
- watchInstances.delete(handle);
1318
- releaseWatchTrampolineIfIdle();
1319
- }
1320
- /**
1321
- * Health check.
1322
- *
1323
- * `handle` can be null for a limited check (version + git only).
1324
- * When null, we pass DataType.U64 with value 0 as a null pointer workaround
1325
- * since ffi-rs does not accept `null` for External parameters.
1326
- */
1327
- export function ffiHealthCheck(handle, testPath) {
1328
- if (handle === null) {
1329
- // Use U64(0) as a null pointer since ffi-rs rejects null for External params
1330
- return callJsonResult("fff_health_check", [DataType.U64, DataType.String], [0, testPath]);
1331
- }
1332
- return callJsonResult("fff_health_check", [DataType.External, DataType.String], [handle, testPath]);
1333
- }
1334
- /**
1335
- * Ensure the library is loaded.
1336
- *
1337
- * Loads the native library from the platform-specific npm package
1338
- * or a local dev build. Throws if the binary is not found.
1339
- */
1340
- export function ensureLoaded() {
1341
- loadLibrary();
1342
- }
1343
- /**
1344
- * Check if the library is available.
1345
- */
1346
- export function isAvailable() {
1347
- try {
1348
- loadLibrary();
1349
- return true;
1350
- }
1351
- catch {
1352
- return false;
1353
- }
1354
- }
1355
- /**
1356
- * Close the library and release ffi-rs resources.
1357
- * Call this when completely done with the library.
1358
- */
1359
- export function closeLibrary() {
1360
- if (isLoaded) {
1361
- close(LIBRARY_KEY);
1362
- isLoaded = false;
1363
- }
1364
- }
1365
- //# sourceMappingURL=ffi.js.map