| 234 | * without blocking on a sync (issue #403) |
| 235 | */ |
| 236 | export class FileWatcher { |
| 237 | /** macOS/Windows: the single recursive watcher. Null on Linux. */ |
| 238 | private recursiveWatcher: fs.FSWatcher | null = null; |
| 239 | /** Linux: one watcher per watched directory (keyed by absolute path). */ |
| 240 | private dirWatchers = new Map<string, fs.FSWatcher>(); |
| 241 | /** Set once the per-directory watch cap is hit, so we log only once. */ |
| 242 | private dirCapWarned = false; |
| 243 | /** |
| 244 | * Set once the Linux inotify watch limit (ENOSPC) is hit. Double duty: we |
| 245 | * warn only once, AND we stop attempting new directory watches for the rest |
| 246 | * of the session — once the kernel budget is exhausted every further |
| 247 | * `inotify_add_watch` fails too, so trying the rest of the tree is pure |
| 248 | * waste. NON-fatal (does not degrade): installed watches keep working. |
| 249 | */ |
| 250 | private inotifyLimitWarned = false; |
| 251 | /** |
| 252 | * One-way latch: the reason live watching was permanently disabled at runtime |
| 253 | * (watch-resource exhaustion, or lock contention past the retry budget), or |
| 254 | * null while healthy. Set by {@link degrade}; cleared only by a fresh start(). |
| 255 | */ |
| 256 | private degradedReason: string | null = null; |
| 257 | /** Consecutive lock-contention retries for watcher-triggered syncs. */ |
| 258 | private lockRetryCount = 0; |
| 259 | /** Test-only inert mode: started, but with no OS watcher installed. */ |
| 260 | private inert = false; |
| 261 | private debounceTimer: ReturnType<typeof setTimeout> | null = null; |
| 262 | /** |
| 263 | * Files seen by the watcher since the last successful sync — populated on |
| 264 | * every change event, cleared at the start of a sync, and re-populated by |
| 265 | * events that arrive mid-sync (or restored on sync failure). Keyed by the |
| 266 | * same project-relative POSIX path the rest of the codebase uses, so a |
| 267 | * caller can intersect tool-response file paths against this map cheaply. |
| 268 | */ |
| 269 | private pendingFiles = new Map<string, { firstSeenMs: number; lastSeenMs: number }>(); |
| 270 | /** |
| 271 | * Wall-clock ms at which the in-flight sync began. Combined with |
| 272 | * {@link pendingFiles}'s `lastSeenMs`, this distinguishes "still in the |
| 273 | * debounce window" (lastSeen > syncStarted, sync hasn't started yet for |
| 274 | * this edit) from "currently being indexed" (lastSeen <= syncStarted). |
| 275 | */ |
| 276 | private syncStartedMs = 0; |
| 277 | private syncing = false; |
| 278 | private stopped = false; |
| 279 | /** |
| 280 | * True once the initial watch set is established. Unlike the previous |
| 281 | * chokidar implementation there is no asynchronous initial "crawl" emitting |
| 282 | * an `add` per existing file — `fs.watch` only reports changes from the |
| 283 | * moment it's installed — so this flips to true synchronously at the end of |
| 284 | * `start()`. The startup reconcile against on-disk state is handled |
| 285 | * separately by the engine's catch-up sync, not by the watcher. |
| 286 | */ |
| 287 | private ready = false; |
| 288 | /** |
| 289 | * Callbacks that resolve when the watch set is established. Used by tests |
| 290 | * (and any production caller that cares about a clean baseline) to |
| 291 | * deterministically gate on watcher readiness. |
| 292 | */ |
| 293 | private readyWaiters: Array<() => void> = []; |
nothing calls this directly
no outgoing calls
no test coverage detected