@ff-labs/fff-bun 0.4.2 → 0.4.3-nightly.2dc8b30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ff-labs/fff-bun",
3
- "version": "0.4.2",
3
+ "version": "0.4.3-nightly.2dc8b30",
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.2",
66
- "@ff-labs/fff-bin-darwin-x64": "0.4.2",
67
- "@ff-labs/fff-bin-linux-x64-gnu": "0.4.2",
68
- "@ff-labs/fff-bin-linux-arm64-gnu": "0.4.2",
69
- "@ff-labs/fff-bin-linux-x64-musl": "0.4.2",
70
- "@ff-labs/fff-bin-linux-arm64-musl": "0.4.2",
71
- "@ff-labs/fff-bin-win32-x64": "0.4.2",
72
- "@ff-labs/fff-bin-win32-arm64": "0.4.2"
65
+ "@ff-labs/fff-bin-darwin-arm64": "0.4.3-nightly.2dc8b30",
66
+ "@ff-labs/fff-bin-darwin-x64": "0.4.3-nightly.2dc8b30",
67
+ "@ff-labs/fff-bin-linux-x64-gnu": "0.4.3-nightly.2dc8b30",
68
+ "@ff-labs/fff-bin-linux-arm64-gnu": "0.4.3-nightly.2dc8b30",
69
+ "@ff-labs/fff-bin-linux-x64-musl": "0.4.3-nightly.2dc8b30",
70
+ "@ff-labs/fff-bin-linux-arm64-musl": "0.4.3-nightly.2dc8b30",
71
+ "@ff-labs/fff-bin-win32-x64": "0.4.3-nightly.2dc8b30",
72
+ "@ff-labs/fff-bin-win32-arm64": "0.4.3-nightly.2dc8b30"
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 = 0; // bool (1 + 7 padding)
257
- const RES_ERROR = 8; // *mut c_char (8)
258
- const RES_HANDLE = 16; // *mut c_void (8)
259
- const RES_INT_VALUE = 24; // i64 (8)
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(resultPtr: Pointer | null): { success: true; handlePtr: number; intValue: number } | Result<never> {
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+7pad) }
827
- const SP_COUNT = 0; // u64 (8)
828
- const SP_SCANNING = 8; // bool (1 + 7 pad)
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<{ scannedFilesCount: number; isScanning: boolean }> {
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.fff_wait_for_scan(handle, BigInt(timeoutMs));
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
-