CodeGraph FileWatcher: How It Detects Changes and Manages Debouncing
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 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 monitoredconfig– ACodeGraphConfigobject containing include/exclude glob patternssyncFn– An async callback that executes the actual graph synchronizationWatchOptions– Optional configuration includingdebounceMs,onSyncComplete, andonSyncError
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 thesyncFnthrows 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):
- Path normalization via
normalizePath(fromsrc/utils.ts) to standardize separators across operating systems - Directory exclusion – Events inside the internal
.codegraph/metadata folder are ignored (lines 95‑101) - Pattern matching – The
shouldIncludeFilefunction (fromsrc/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 activesetTimeouthandlehasChanges– Boolean flag indicating unprocessed events
When a filtered file event occurs, scheduleSync() executes (lines 61‑67):
- Clears any existing
debounceTimer - Sets
hasChanges = true - 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):
- Guard clauses abort if a sync is already running (
syncingflag) or if the watcher has stopped - Resets
hasChangestofalseand setssyncing = true - Executes the provided
syncFn– typically bound toCodeGraph.sync() - On success, calls
onSyncComplete; on failure, logs the warning and invokesonSyncError - Clears the
syncingflag - Post-sync check: If
hasChangesbecame 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 thefs.FSWatcher, returnsfalseif 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:
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 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:
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
FileWatcherclass insrc/sync/watcher.tsprovides the automatic synchronization engine for CodeGraph projects - It uses Node.js
fs.watchwith recursive mode for cross-platform filesystem monitoring - Changes are filtered through
normalizePath,.codegraph/exclusion, andshouldIncludeFilepattern matching - Debouncing defaults to 2000ms and is managed through
scheduleSync()andflush()methods to batch rapid changes - The
syncingflag andhasChangestracking 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. 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →