@ff-labs/fff-bun 0.1.0-nightly.044314f

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/src/ffi.ts ADDED
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Bun FFI bindings for the fff-c native library
3
+ *
4
+ * This module uses Bun's native FFI to call into the Rust C library.
5
+ * All functions follow the Result pattern for error handling.
6
+ */
7
+
8
+ import { dlopen, FFIType, ptr, CString, read, type Pointer } from "bun:ffi";
9
+ import { findBinary, ensureBinary } from "./download";
10
+ import type { Result } from "./types";
11
+ import { err } from "./types";
12
+
13
+ // Define the FFI symbols
14
+ const ffiDefinition = {
15
+ // Lifecycle
16
+ fff_init: {
17
+ args: [FFIType.cstring],
18
+ returns: FFIType.ptr,
19
+ },
20
+ fff_destroy: {
21
+ args: [],
22
+ returns: FFIType.ptr,
23
+ },
24
+
25
+ // Search
26
+ fff_search: {
27
+ args: [FFIType.cstring, FFIType.cstring],
28
+ returns: FFIType.ptr,
29
+ },
30
+
31
+ // Live grep (content search)
32
+ fff_live_grep: {
33
+ args: [FFIType.cstring, FFIType.cstring],
34
+ returns: FFIType.ptr,
35
+ },
36
+
37
+ // File index
38
+ fff_scan_files: {
39
+ args: [],
40
+ returns: FFIType.ptr,
41
+ },
42
+ fff_is_scanning: {
43
+ args: [],
44
+ returns: FFIType.bool,
45
+ },
46
+ fff_get_scan_progress: {
47
+ args: [],
48
+ returns: FFIType.ptr,
49
+ },
50
+ fff_wait_for_scan: {
51
+ args: [FFIType.u64],
52
+ returns: FFIType.ptr,
53
+ },
54
+ fff_restart_index: {
55
+ args: [FFIType.cstring],
56
+ returns: FFIType.ptr,
57
+ },
58
+
59
+ // Frecency
60
+ fff_track_access: {
61
+ args: [FFIType.cstring],
62
+ returns: FFIType.ptr,
63
+ },
64
+
65
+ // Git
66
+ fff_refresh_git_status: {
67
+ args: [],
68
+ returns: FFIType.ptr,
69
+ },
70
+
71
+ // Query tracking
72
+ fff_track_query: {
73
+ args: [FFIType.cstring, FFIType.cstring],
74
+ returns: FFIType.ptr,
75
+ },
76
+ fff_get_historical_query: {
77
+ args: [FFIType.u64],
78
+ returns: FFIType.ptr,
79
+ },
80
+
81
+ // Utilities
82
+ fff_health_check: {
83
+ args: [FFIType.cstring],
84
+ returns: FFIType.ptr,
85
+ },
86
+
87
+ // Memory management
88
+ fff_free_result: {
89
+ args: [FFIType.ptr],
90
+ returns: FFIType.void,
91
+ },
92
+ fff_free_string: {
93
+ args: [FFIType.ptr],
94
+ returns: FFIType.void,
95
+ },
96
+ } as const;
97
+
98
+ type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
99
+
100
+ // Library instance (lazy loaded)
101
+ let lib: FFFLibrary | null = null;
102
+
103
+ /**
104
+ * Load the native library
105
+ */
106
+ function loadLibrary(): FFFLibrary {
107
+ if (lib) return lib;
108
+
109
+ const binaryPath = findBinary();
110
+ if (!binaryPath) {
111
+ throw new Error(
112
+ "fff native library not found. Run `bunx fff download` or build from source with `cargo build --release -p fff-c`"
113
+ );
114
+ }
115
+
116
+ lib = dlopen(binaryPath, ffiDefinition);
117
+ return lib;
118
+ }
119
+
120
+ /**
121
+ * Encode a string for FFI (null-terminated)
122
+ */
123
+ function encodeString(s: string): Uint8Array {
124
+ return new TextEncoder().encode(s + "\0");
125
+ }
126
+
127
+ /**
128
+ * Read a C string from a pointer
129
+ * Note: read.ptr() returns number but CString expects Pointer - we cast through unknown
130
+ */
131
+ function readCString(pointer: Pointer | number | null): string | null {
132
+ if (pointer === null || pointer === 0) return null;
133
+ // CString constructor accepts Pointer, but read.ptr returns number
134
+ // Cast through unknown for runtime compatibility
135
+ return new CString(pointer as unknown as Pointer).toString();
136
+ }
137
+
138
+ /**
139
+ * Convert snake_case keys to camelCase recursively
140
+ */
141
+ function snakeToCamel(obj: unknown): unknown {
142
+ if (obj === null || obj === undefined) return obj;
143
+ if (typeof obj !== "object") return obj;
144
+ if (Array.isArray(obj)) return obj.map(snakeToCamel);
145
+
146
+ const result: Record<string, unknown> = {};
147
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
148
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
149
+ letter.toUpperCase()
150
+ );
151
+ result[camelKey] = snakeToCamel(value);
152
+ }
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Parse a FffResult from the FFI return value
158
+ * The result is a pointer to a struct: { success: bool, data: *char, error: *char }
159
+ */
160
+ function parseResult<T>(resultPtr: Pointer | null): Result<T> {
161
+ if (resultPtr === null) {
162
+ return err("FFI returned null pointer");
163
+ }
164
+
165
+ // Read the struct fields
166
+ // FffResult layout: bool (1 byte + 7 padding) + pointer (8 bytes) + pointer (8 bytes)
167
+ // offset 0: success (bool, 1 byte)
168
+ // offset 8: data pointer (8 bytes)
169
+ // offset 16: error pointer (8 bytes)
170
+ const success = read.u8(resultPtr, 0) !== 0;
171
+ const dataPtr = read.ptr(resultPtr, 8);
172
+ const errorPtr = read.ptr(resultPtr, 16);
173
+
174
+ const library = loadLibrary();
175
+
176
+ if (success) {
177
+ const data = readCString(dataPtr);
178
+ // Free the result
179
+ library.symbols.fff_free_result(resultPtr);
180
+
181
+ if (data === null || data === "") {
182
+ return { ok: true, value: undefined as T };
183
+ }
184
+
185
+ try {
186
+ const parsed = JSON.parse(data);
187
+ // Convert snake_case to camelCase for TypeScript consumers
188
+ const transformed = snakeToCamel(parsed) as T;
189
+ return { ok: true, value: transformed };
190
+ } catch {
191
+ // For simple values like "true" or numbers
192
+ return { ok: true, value: data as T };
193
+ }
194
+ } else {
195
+ const errorMsg = readCString(errorPtr) || "Unknown error";
196
+ // Free the result
197
+ library.symbols.fff_free_result(resultPtr);
198
+ return err(errorMsg);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Initialize the file finder
204
+ */
205
+ export function ffiInit(optsJson: string): Result<void> {
206
+ const library = loadLibrary();
207
+ const resultPtr = library.symbols.fff_init(ptr(encodeString(optsJson)));
208
+ return parseResult<void>(resultPtr);
209
+ }
210
+
211
+ /**
212
+ * Destroy and clean up resources
213
+ */
214
+ export function ffiDestroy(): Result<void> {
215
+ const library = loadLibrary();
216
+ const resultPtr = library.symbols.fff_destroy();
217
+ return parseResult<void>(resultPtr);
218
+ }
219
+
220
+ /**
221
+ * Perform fuzzy search
222
+ */
223
+ export function ffiSearch(query: string, optsJson: string): Result<unknown> {
224
+ const library = loadLibrary();
225
+ const resultPtr = library.symbols.fff_search(
226
+ ptr(encodeString(query)),
227
+ ptr(encodeString(optsJson))
228
+ );
229
+ return parseResult<unknown>(resultPtr);
230
+ }
231
+
232
+ /**
233
+ * Trigger file scan
234
+ */
235
+ export function ffiScanFiles(): Result<void> {
236
+ const library = loadLibrary();
237
+ const resultPtr = library.symbols.fff_scan_files();
238
+ return parseResult<void>(resultPtr);
239
+ }
240
+
241
+ /**
242
+ * Check if scanning
243
+ */
244
+ export function ffiIsScanning(): boolean {
245
+ const library = loadLibrary();
246
+ return library.symbols.fff_is_scanning() as boolean;
247
+ }
248
+
249
+ /**
250
+ * Get scan progress
251
+ */
252
+ export function ffiGetScanProgress(): Result<unknown> {
253
+ const library = loadLibrary();
254
+ const resultPtr = library.symbols.fff_get_scan_progress();
255
+ return parseResult<unknown>(resultPtr);
256
+ }
257
+
258
+ /**
259
+ * Wait for scan to complete
260
+ */
261
+ export function ffiWaitForScan(timeoutMs: number): Result<boolean> {
262
+ const library = loadLibrary();
263
+ const resultPtr = library.symbols.fff_wait_for_scan(BigInt(timeoutMs));
264
+ const result = parseResult<string>(resultPtr);
265
+ if (!result.ok) return result;
266
+ return { ok: true, value: result.value === "true" };
267
+ }
268
+
269
+ /**
270
+ * Restart index in new path
271
+ */
272
+ export function ffiRestartIndex(newPath: string): Result<void> {
273
+ const library = loadLibrary();
274
+ const resultPtr = library.symbols.fff_restart_index(
275
+ ptr(encodeString(newPath))
276
+ );
277
+ return parseResult<void>(resultPtr);
278
+ }
279
+
280
+ /**
281
+ * Track file access
282
+ */
283
+ export function ffiTrackAccess(filePath: string): Result<boolean> {
284
+ const library = loadLibrary();
285
+ const resultPtr = library.symbols.fff_track_access(
286
+ ptr(encodeString(filePath))
287
+ );
288
+ const result = parseResult<string>(resultPtr);
289
+ if (!result.ok) return result;
290
+ return { ok: true, value: result.value === "true" };
291
+ }
292
+
293
+ /**
294
+ * Refresh git status
295
+ */
296
+ export function ffiRefreshGitStatus(): Result<number> {
297
+ const library = loadLibrary();
298
+ const resultPtr = library.symbols.fff_refresh_git_status();
299
+ const result = parseResult<string>(resultPtr);
300
+ if (!result.ok) return result;
301
+ return { ok: true, value: parseInt(result.value, 10) };
302
+ }
303
+
304
+ /**
305
+ * Track query completion
306
+ */
307
+ export function ffiTrackQuery(
308
+ query: string,
309
+ filePath: string
310
+ ): Result<boolean> {
311
+ const library = loadLibrary();
312
+ const resultPtr = library.symbols.fff_track_query(
313
+ ptr(encodeString(query)),
314
+ ptr(encodeString(filePath))
315
+ );
316
+ const result = parseResult<string>(resultPtr);
317
+ if (!result.ok) return result;
318
+ return { ok: true, value: result.value === "true" };
319
+ }
320
+
321
+ /**
322
+ * Get historical query
323
+ */
324
+ export function ffiGetHistoricalQuery(offset: number): Result<string | null> {
325
+ const library = loadLibrary();
326
+ const resultPtr = library.symbols.fff_get_historical_query(BigInt(offset));
327
+ const result = parseResult<string>(resultPtr);
328
+ if (!result.ok) return result;
329
+ if (result.value === "null") return { ok: true, value: null };
330
+ return result;
331
+ }
332
+
333
+ /**
334
+ * Health check
335
+ */
336
+ export function ffiHealthCheck(testPath: string): Result<unknown> {
337
+ const library = loadLibrary();
338
+ const resultPtr = library.symbols.fff_health_check(
339
+ ptr(encodeString(testPath))
340
+ );
341
+ return parseResult<unknown>(resultPtr);
342
+ }
343
+
344
+ /**
345
+ * Live grep - search file contents
346
+ */
347
+ export function ffiLiveGrep(query: string, optsJson: string): Result<unknown> {
348
+ const library = loadLibrary();
349
+ const resultPtr = library.symbols.fff_live_grep(
350
+ ptr(encodeString(query)),
351
+ ptr(encodeString(optsJson))
352
+ );
353
+ return parseResult<unknown>(resultPtr);
354
+ }
355
+
356
+ /**
357
+ * Ensure the library is loaded (for preloading)
358
+ */
359
+ export async function ensureLoaded(): Promise<void> {
360
+ await ensureBinary();
361
+ loadLibrary();
362
+ }
363
+
364
+ /**
365
+ * Check if the library is available
366
+ */
367
+ export function isAvailable(): boolean {
368
+ try {
369
+ loadLibrary();
370
+ return true;
371
+ } catch {
372
+ return false;
373
+ }
374
+ }