@ff-labs/fff-bun 0.4.2 → 0.4.3-nightly.371d54a
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/package.json +9 -9
- package/src/ffi.ts +36 -27
- package/src/finder.ts +17 -1
- package/src/git-lifecycle.test.ts +15 -1
- package/src/types.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/fff-bun",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3-nightly.371d54a",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
|
|
6
6
|
"type": "module",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@ff-labs/fff-bin-darwin-arm64": "0.4.
|
|
66
|
-
"@ff-labs/fff-bin-darwin-x64": "0.4.
|
|
67
|
-
"@ff-labs/fff-bin-linux-x64-gnu": "0.4.
|
|
68
|
-
"@ff-labs/fff-bin-linux-arm64-gnu": "0.4.
|
|
69
|
-
"@ff-labs/fff-bin-linux-x64-musl": "0.4.
|
|
70
|
-
"@ff-labs/fff-bin-linux-arm64-musl": "0.4.
|
|
71
|
-
"@ff-labs/fff-bin-win32-x64": "0.4.
|
|
72
|
-
"@ff-labs/fff-bin-win32-arm64": "0.4.
|
|
65
|
+
"@ff-labs/fff-bin-darwin-arm64": "0.4.3-nightly.371d54a",
|
|
66
|
+
"@ff-labs/fff-bin-darwin-x64": "0.4.3-nightly.371d54a",
|
|
67
|
+
"@ff-labs/fff-bin-linux-x64-gnu": "0.4.3-nightly.371d54a",
|
|
68
|
+
"@ff-labs/fff-bin-linux-arm64-gnu": "0.4.3-nightly.371d54a",
|
|
69
|
+
"@ff-labs/fff-bin-linux-x64-musl": "0.4.3-nightly.371d54a",
|
|
70
|
+
"@ff-labs/fff-bin-linux-arm64-musl": "0.4.3-nightly.371d54a",
|
|
71
|
+
"@ff-labs/fff-bin-win32-x64": "0.4.3-nightly.371d54a",
|
|
72
|
+
"@ff-labs/fff-bin-win32-arm64": "0.4.3-nightly.371d54a"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/bun": "^1.3.8",
|
package/src/ffi.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
GrepResult,
|
|
17
17
|
Location,
|
|
18
18
|
Result,
|
|
19
|
+
ScanProgress,
|
|
19
20
|
Score,
|
|
20
21
|
SearchResult,
|
|
21
22
|
} from "./types";
|
|
@@ -125,6 +126,10 @@ const ffiDefinition = {
|
|
|
125
126
|
args: [FFIType.ptr, FFIType.u64],
|
|
126
127
|
returns: FFIType.ptr,
|
|
127
128
|
},
|
|
129
|
+
fff_wait_for_watcher: {
|
|
130
|
+
args: [FFIType.ptr, FFIType.u64],
|
|
131
|
+
returns: FFIType.ptr,
|
|
132
|
+
},
|
|
128
133
|
fff_restart_index: {
|
|
129
134
|
args: [FFIType.ptr, FFIType.cstring],
|
|
130
135
|
returns: FFIType.ptr,
|
|
@@ -241,9 +246,7 @@ function snakeToCamel(obj: unknown): unknown {
|
|
|
241
246
|
|
|
242
247
|
const result: Record<string, unknown> = {};
|
|
243
248
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
244
|
-
const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
|
|
245
|
-
letter.toUpperCase(),
|
|
246
|
-
);
|
|
249
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
247
250
|
result[camelKey] = snakeToCamel(value);
|
|
248
251
|
}
|
|
249
252
|
return result;
|
|
@@ -253,16 +256,18 @@ function snakeToCamel(obj: unknown): unknown {
|
|
|
253
256
|
// FffResult byte offsets (must match #[repr(C)] layout on 64-bit)
|
|
254
257
|
// { success: bool(1+7pad), error: *char(8), handle: *void(8), int_value: i64(8) }
|
|
255
258
|
// ---------------------------------------------------------------------------
|
|
256
|
-
const RES_SUCCESS
|
|
257
|
-
const RES_ERROR
|
|
258
|
-
const RES_HANDLE
|
|
259
|
-
const RES_INT_VALUE = 24;
|
|
259
|
+
const RES_SUCCESS = 0; // bool (1 + 7 padding)
|
|
260
|
+
const RES_ERROR = 8; // *mut c_char (8)
|
|
261
|
+
const RES_HANDLE = 16; // *mut c_void (8)
|
|
262
|
+
const RES_INT_VALUE = 24; // i64 (8)
|
|
260
263
|
|
|
261
264
|
/**
|
|
262
265
|
* Read the FffResult envelope: check success, extract payload, free envelope.
|
|
263
266
|
* On error returns a Result<never>. On success returns the raw handle pointer and int_value.
|
|
264
267
|
*/
|
|
265
|
-
function readResultEnvelope(
|
|
268
|
+
function readResultEnvelope(
|
|
269
|
+
resultPtr: Pointer | null,
|
|
270
|
+
): { success: true; handlePtr: number; intValue: number } | Result<never> {
|
|
266
271
|
if (resultPtr === null) {
|
|
267
272
|
return err("FFI returned null pointer");
|
|
268
273
|
}
|
|
@@ -651,16 +656,10 @@ function readGrepMatchStruct(p: number): GrepMatch {
|
|
|
651
656
|
match.fuzzyScore = read.u16(pp, GM_FUZZY_SCORE);
|
|
652
657
|
}
|
|
653
658
|
if (ctxBeforeCount > 0) {
|
|
654
|
-
match.contextBefore = readCStringArray(
|
|
655
|
-
read.ptr(pp, GM_CTX_BEFORE),
|
|
656
|
-
ctxBeforeCount,
|
|
657
|
-
);
|
|
659
|
+
match.contextBefore = readCStringArray(read.ptr(pp, GM_CTX_BEFORE), ctxBeforeCount);
|
|
658
660
|
}
|
|
659
661
|
if (ctxAfterCount > 0) {
|
|
660
|
-
match.contextAfter = readCStringArray(
|
|
661
|
-
read.ptr(pp, GM_CTX_AFTER),
|
|
662
|
-
ctxAfterCount,
|
|
663
|
-
);
|
|
662
|
+
match.contextAfter = readCStringArray(read.ptr(pp, GM_CTX_AFTER), ctxAfterCount);
|
|
664
663
|
}
|
|
665
664
|
|
|
666
665
|
return match;
|
|
@@ -823,14 +822,16 @@ export function ffiIsScanning(handle: NativeHandle): boolean {
|
|
|
823
822
|
return library.symbols.fff_is_scanning(handle) as boolean;
|
|
824
823
|
}
|
|
825
824
|
|
|
826
|
-
// FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1
|
|
827
|
-
const SP_COUNT
|
|
828
|
-
const SP_SCANNING = 8;
|
|
825
|
+
// FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1), is_watcher_ready: bool(1), is_warmup_complete: bool(1) + pad }
|
|
826
|
+
const SP_COUNT = 0; // u64 (8)
|
|
827
|
+
const SP_SCANNING = 8; // bool (1)
|
|
828
|
+
const SP_WATCHER_READY = 9; // bool (1)
|
|
829
|
+
const SP_WARMUP_COMPLETE = 10; // bool (1)
|
|
829
830
|
|
|
830
831
|
/**
|
|
831
832
|
* Get scan progress.
|
|
832
833
|
*/
|
|
833
|
-
export function ffiGetScanProgress(handle: NativeHandle): Result<
|
|
834
|
+
export function ffiGetScanProgress(handle: NativeHandle): Result<ScanProgress> {
|
|
834
835
|
const library = loadLibrary();
|
|
835
836
|
const resultPtr = library.symbols.fff_get_scan_progress(handle);
|
|
836
837
|
const envelope = readResultEnvelope(resultPtr);
|
|
@@ -841,9 +842,11 @@ export function ffiGetScanProgress(handle: NativeHandle): Result<{ scannedFilesC
|
|
|
841
842
|
}
|
|
842
843
|
|
|
843
844
|
const hp = asPtr(envelope.handlePtr);
|
|
844
|
-
const result = {
|
|
845
|
+
const result: ScanProgress = {
|
|
845
846
|
scannedFilesCount: Number(read.u64(hp, SP_COUNT)),
|
|
846
847
|
isScanning: read.u8(hp, SP_SCANNING) !== 0,
|
|
848
|
+
isWatcherReady: read.u8(hp, SP_WATCHER_READY) !== 0,
|
|
849
|
+
isWarmupComplete: read.u8(hp, SP_WARMUP_COMPLETE) !== 0,
|
|
847
850
|
};
|
|
848
851
|
library.symbols.fff_free_scan_progress(hp);
|
|
849
852
|
return { ok: true, value: result };
|
|
@@ -852,22 +855,28 @@ export function ffiGetScanProgress(handle: NativeHandle): Result<{ scannedFilesC
|
|
|
852
855
|
/**
|
|
853
856
|
* Wait for scan to complete.
|
|
854
857
|
*/
|
|
855
|
-
export function ffiWaitForScan(
|
|
858
|
+
export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<boolean> {
|
|
859
|
+
const library = loadLibrary();
|
|
860
|
+
const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
|
|
861
|
+
return parseBoolResult(resultPtr);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Wait for the background file watcher to be ready.
|
|
866
|
+
*/
|
|
867
|
+
export function ffiWaitForWatcher(
|
|
856
868
|
handle: NativeHandle,
|
|
857
869
|
timeoutMs: number,
|
|
858
870
|
): Result<boolean> {
|
|
859
871
|
const library = loadLibrary();
|
|
860
|
-
const resultPtr = library.symbols.
|
|
872
|
+
const resultPtr = library.symbols.fff_wait_for_watcher(handle, BigInt(timeoutMs));
|
|
861
873
|
return parseBoolResult(resultPtr);
|
|
862
874
|
}
|
|
863
875
|
|
|
864
876
|
/**
|
|
865
877
|
* Restart index in new path.
|
|
866
878
|
*/
|
|
867
|
-
export function ffiRestartIndex(
|
|
868
|
-
handle: NativeHandle,
|
|
869
|
-
newPath: string,
|
|
870
|
-
): Result<void> {
|
|
879
|
+
export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<void> {
|
|
871
880
|
const library = loadLibrary();
|
|
872
881
|
const resultPtr = library.symbols.fff_restart_index(handle, ptr(encodeString(newPath)));
|
|
873
882
|
return parseVoidResult(resultPtr);
|
package/src/finder.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ffiSearch,
|
|
25
25
|
ffiTrackQuery,
|
|
26
26
|
ffiWaitForScan,
|
|
27
|
+
ffiWaitForWatcher,
|
|
27
28
|
isAvailable,
|
|
28
29
|
type NativeHandle,
|
|
29
30
|
} from "./ffi";
|
|
@@ -345,6 +346,22 @@ export class FileFinder {
|
|
|
345
346
|
return ffiWaitForScan(guard.value, timeoutMs);
|
|
346
347
|
}
|
|
347
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Wait for the background file watcher to be ready.
|
|
351
|
+
*
|
|
352
|
+
* The watcher is created after the initial scan, git status, and optional
|
|
353
|
+
* warmup phases complete. Useful for tests that need to ensure filesystem
|
|
354
|
+
* events will be detected.
|
|
355
|
+
*
|
|
356
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 10000)
|
|
357
|
+
* @returns true if watcher is ready, false if timed out
|
|
358
|
+
*/
|
|
359
|
+
waitForWatcher(timeoutMs: number = 10000): Result<boolean> {
|
|
360
|
+
const guard = this.ensureAlive();
|
|
361
|
+
if (!guard.ok) return guard;
|
|
362
|
+
return ffiWaitForWatcher(guard.value, timeoutMs);
|
|
363
|
+
}
|
|
364
|
+
|
|
348
365
|
/**
|
|
349
366
|
* Change the indexed directory to a new path.
|
|
350
367
|
*
|
|
@@ -430,4 +447,3 @@ export class FileFinder {
|
|
|
430
447
|
return ffiHealthCheck(null, testPath || "") as Result<HealthCheck>;
|
|
431
448
|
}
|
|
432
449
|
}
|
|
433
|
-
|
|
@@ -127,7 +127,7 @@ describe.skipIf(process.platform === "win32")("Git lifecycle integration", () =>
|
|
|
127
127
|
let tmpDir: string;
|
|
128
128
|
let finder: FileFinder;
|
|
129
129
|
|
|
130
|
-
beforeAll(() => {
|
|
130
|
+
beforeAll(async () => {
|
|
131
131
|
// Create temp directory and initialise a git repo with two committed files.
|
|
132
132
|
// Use realpathSync to resolve symlinks (macOS /var -> /private/var) so
|
|
133
133
|
// that git2's resolved workdir paths match the file picker's base_path.
|
|
@@ -151,6 +151,20 @@ describe.skipIf(process.platform === "win32")("Git lifecycle integration", () =>
|
|
|
151
151
|
// Wait for the initial scan to finish
|
|
152
152
|
const scanResult = finder.waitForScan(10_000);
|
|
153
153
|
expect(scanResult.ok).toBe(true);
|
|
154
|
+
|
|
155
|
+
// Poll getScanProgress until the watcher is ready so that
|
|
156
|
+
// filesystem events (file creates, deletes) are detected.
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
|
159
|
+
const progress = finder.getScanProgress();
|
|
160
|
+
if (progress.ok && progress.value.isWatcherReady) break;
|
|
161
|
+
await sleep(POLL_INTERVAL_MS);
|
|
162
|
+
}
|
|
163
|
+
const progress = finder.getScanProgress();
|
|
164
|
+
expect(progress.ok).toBe(true);
|
|
165
|
+
if (progress.ok) {
|
|
166
|
+
expect(progress.value.isWatcherReady).toBe(true);
|
|
167
|
+
}
|
|
154
168
|
});
|
|
155
169
|
|
|
156
170
|
afterAll(() => {
|
package/src/types.ts
CHANGED
|
@@ -144,6 +144,10 @@ export interface ScanProgress {
|
|
|
144
144
|
scannedFilesCount: number;
|
|
145
145
|
/** Whether a scan is currently in progress */
|
|
146
146
|
isScanning: boolean;
|
|
147
|
+
/** Whether the background file watcher is ready */
|
|
148
|
+
isWatcherReady: boolean;
|
|
149
|
+
/** Whether the warmup/bigram phase has completed */
|
|
150
|
+
isWarmupComplete: boolean;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
/**
|
|
@@ -360,4 +364,3 @@ export interface MultiGrepOptions {
|
|
|
360
364
|
/** Number of context lines to include after each match (default: 0) */
|
|
361
365
|
afterContext?: number;
|
|
362
366
|
}
|
|
363
|
-
|