@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/README.md +283 -0
- package/examples/grep.ts +283 -0
- package/examples/search.ts +222 -0
- package/package.json +86 -0
- package/scripts/cli.ts +114 -0
- package/scripts/postinstall.ts +51 -0
- package/src/download.ts +230 -0
- package/src/ffi.ts +374 -0
- package/src/finder.ts +380 -0
- package/src/index.test.ts +230 -0
- package/src/index.ts +74 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +430 -0
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
|
+
}
|