# CodeGraph FileWatcher: How It Detects Changes and Manages Debouncing

> Learn how CodeGraph's FileWatcher detects file changes using Node.js fs.watch. Discover its debouncing technique for efficient change management.

- Repository: [Colby Mchenry/codegraph](https://github.com/colbymchenry/codegraph)
- Tags: internals
- Published: 2026-05-17

---

**The FileWatcher class in CodeGraph uses Node.js `fs.watch` with recursive monitoring, filters events through include/exclude patterns, and batches rapid changes using a debounce timer that defaults to 2000ms before triggering the sync function.**

The `FileWatcher` is the core automatic-sync mechanism in the `colbymchenry/codegraph` repository, responsible for keeping the code graph synchronized with filesystem modifications. When you enable watching on a CodeGraph project, this class handles the heavy lifting of detecting file changes, filtering noise, and efficiently batching updates to prevent system overload.

## FileWatcher Architecture and Configuration

The `FileWatcher` class is implemented in **[`src/sync/watcher.ts`](https://github.com/colbymchenry/codegraph/blob/main/src/sync/watcher.ts)** and designed as a self-contained module that bridges OS-level file events and the graph's indexing system.

### Constructor Parameters and Defaults

When instantiating a `FileWatcher`, you provide four critical components (lines 56‑74):

- **`projectRoot`** – Absolute path to the directory being monitored
- **`config`** – A `CodeGraphConfig` object containing include/exclude glob patterns
- **`syncFn`** – An async callback that executes the actual graph synchronization
- **`WatchOptions`** – Optional configuration including `debounceMs`, `onSyncComplete`, and `onSyncError`

If you omit `debounceMs`, the watcher defaults to **2000 ms** (2 seconds), ensuring that rapid bursts of file saves do not trigger excessive re-indexing operations.

### Optional Callbacks for Sync Events

The `WatchOptions` interface defines two optional hooks for monitoring synchronization health:

- **`onSyncComplete`** – Invoked with metadata about the sync operation (files changed, duration)
- **`onSyncError`** – Called when the `syncFn` throws an exception, receiving the error object for logging or recovery

## File System Detection and Event Filtering

The watcher leverages Node.js native APIs for cross-platform filesystem monitoring, then applies intelligent filtering to reduce noise.

### Recursive File Watching Implementation

Calling `watcher.start()` initializes the monitoring session using `fs.watch(projectRoot, { recursive: true }, …)` (lines 88‑93). This recursive mode utilizes:

- **macOS**: FSEvents for efficient tree watching
- **Windows**: ReadDirectoryChangesW API
- **Linux**: inotify (requires Node.js 19+ for recursive support)

The callback receives `(eventType, filename)` pairs for every detected modification.

### Path Normalization and Inclusion Logic

For each filesystem event, the watcher performs three validation steps (lines 92‑106):

1. **Path normalization** via `normalizePath` (from **[`src/utils.ts`](https://github.com/colbymchenry/codegraph/blob/main/src/utils.ts)**) to standardize separators across operating systems
2. **Directory exclusion** – Events inside the internal `.codegraph/` metadata folder are ignored (lines 95‑101)
3. **Pattern matching** – The `shouldIncludeFile` function (from **[`src/extraction/index.ts`](https://github.com/colbymchenry/codegraph/blob/main/src/extraction/index.ts)**) validates paths against your configured include/exclude globs

Only events passing all three filters trigger the debounce mechanism.

## Debouncing Implementation and Change Batching

The debouncing system prevents the graph from re-indexing on every keystroke, instead waiting for a pause in file activity before executing the sync routine.

### Debounce Timer Mechanics

Two private state variables control the timing logic:

- **`debounceTimer`** – The active `setTimeout` handle
- **`hasChanges`** – Boolean flag indicating unprocessed events

When a filtered file event occurs, `scheduleSync()` executes (lines 61‑67):

1. Clears any existing `debounceTimer`
2. Sets `hasChanges = true`
3. Creates a new timer set to `debounceMs`

This reset-on-activity pattern ensures the sync only fires after the configured milliseconds of filesystem silence.

### Flush Execution and Concurrency Guard

When the debounce timer expires, `flush()` handles the synchronization (lines 74‑93):

1. **Guard clauses** abort if a sync is already running (`syncing` flag) or if the watcher has stopped
2. Resets `hasChanges` to `false` and sets `syncing = true`
3. Executes the provided `syncFn` – typically bound to `CodeGraph.sync()`
4. On success, calls `onSyncComplete`; on failure, logs the warning and invokes `onSyncError`
5. Clears the `syncing` flag
6. **Post-sync check**: If `hasChanges` became true during the async operation, immediately schedules another debounce cycle to capture missed events

This architecture guarantees **single execution** for rapid change bursts while preventing change loss during slow sync operations.

## Lifecycle Management Methods

The `FileWatcher` provides explicit controls for resource management:

- **`start()`** – Attaches the `fs.FSWatcher`, returns `false` if the OS lacks recursive watch support (lines 124‑127)
- **`stop()`** – Clears the debounce timer, closes the file watcher handle, and resets internal state (lines 132‑148)
- **`isActive()`** – Returns boolean indicating whether the watcher is currently attached to the filesystem (lines 151‑155)

Proper cleanup via `stop()` is essential to prevent memory leaks and dangling file handles.

## Integration with the CodeGraph API

While you can instantiate `FileWatcher` directly, most users interact with it through the high-level CodeGraph interface:

```typescript
import CodeGraph from 'codegraph';

const cg = CodeGraph.initSync('/my/project', { 
  config: { include: ['**/*.ts'] } 
});

await cg.indexAll();               
cg.watch({ debounceMs: 300 });    
// ... work on files ...
cg.unwatch();                      
cg.close();                         

```

Under the hood, `cg.watch()` creates a `FileWatcher` instance bound to the project's configuration and `CodeGraph.sync()` method. The **[`__tests__/watcher.test.ts`](https://github.com/colbymchenry/codegraph/blob/main/__tests__/watcher.test.ts)** suite verifies this integration, testing scenarios including rapid file creation (lines 70‑86) and proper callback invocation.

### Standalone Usage Example

For custom implementations, import the class directly:

```typescript
import { FileWatcher } from 'codegraph/src/sync/watcher';
import type { CodeGraphConfig } from 'codegraph/src/types';

const watcher = new FileWatcher(
  '/my/project',
  config,
  async () => await codeGraph.sync(),
  {
    debounceMs: 1000,
    onSyncComplete: ({ filesChanged, durationMs }) => {
      console.log(`Synced ${filesChanged} files in ${durationMs}ms`);
    },
    onSyncError: (err) => console.error('Sync failed:', err)
  }
);

watcher.start();
// ... later ...
watcher.stop();

```

## Summary

- The `FileWatcher` class in **[`src/sync/watcher.ts`](https://github.com/colbymchenry/codegraph/blob/main/src/sync/watcher.ts)** provides the automatic synchronization engine for CodeGraph projects
- It uses Node.js `fs.watch` with recursive mode for cross-platform filesystem monitoring
- Changes are filtered through `normalizePath`, `.codegraph/` exclusion, and `shouldIncludeFile` pattern matching
- Debouncing defaults to **2000ms** and is managed through `scheduleSync()` and `flush()` methods to batch rapid changes
- The `syncing` flag and `hasChanges` tracking prevent concurrent executions and ensure no events are lost during slow syncs
- Access the watcher through `CodeGraph.watch()` or instantiate directly for custom control flows

## Frequently Asked Questions

### How does CodeGraph prevent excessive re-indexing when multiple files change simultaneously?

The `FileWatcher` implements debouncing through the `scheduleSync()` method, which resets a timer (defaulting to 2000ms) every time a new filesystem event occurs. Only when the timer expires without new events does `flush()` execute the actual `syncFn`. This batches rapid changes into a single graph update, preventing CPU thrashing during file explosions like `git checkout` or bulk saves.

### What happens if file changes occur while the sync function is still running?

The `flush()` method includes a post-sync check that examines the `hasChanges` flag. If new events arrived during the asynchronous `syncFn` execution, `scheduleSync()` is called immediately upon completion, ensuring those changes enter the next debounce cycle. The `syncing` boolean prevents concurrent executions, guaranteeing only one sync runs at a time.

### Can I exclude specific directories or file types from triggering syncs?

Yes. The watcher respects your `CodeGraphConfig` include/exclude patterns through the `shouldIncludeFile` utility from **[`src/extraction/index.ts`](https://github.com/colbymchenry/codegraph/blob/main/src/extraction/index.ts)**. Additionally, the watcher automatically ignores any changes inside the `.codegraph/` metadata directory. You can configure these patterns when initializing CodeGraph or pass them directly to the `FileWatcher` constructor.

### What is the minimum Node.js version required for the file watcher to work on Linux?

Recursive file watching via `fs.watch` requires Node.js 19 or later on Linux systems to utilize the inotify subsystem. Earlier versions lack recursive support in the native `fs` module. The `start()` method catches initialization failures and returns `false` if the OS does not support recursive watching, allowing your application to handle the fallback gracefully.