| 261 | * without blocking on a sync (issue #403) |
| 262 | */ |
| 263 | export class FileWatcher { |
| 264 | /** macOS/Windows: the single recursive watcher. Null on Linux. */ |
| 265 | private recursiveWatcher: fs.FSWatcher | null = null; |
| 266 | /** Linux: one watcher per watched directory (keyed by absolute path). */ |
| 267 | private dirWatchers = new Map<string, fs.FSWatcher>(); |
| 268 | /** Set once the per-directory watch cap is hit, so we log only once. */ |
| 269 | private dirCapWarned = false; |
| 270 | /** |
| 271 | * Set once the Linux inotify watch limit (ENOSPC) is hit. Double duty: we |
| 272 | * warn only once, AND we stop attempting new directory watches for the rest |
| 273 | * of the session — once the kernel budget is exhausted every further |
| 274 | * `inotify_add_watch` fails too, so trying the rest of the tree is pure |
| 275 | * waste. NON-fatal (does not degrade): installed watches keep working. |
| 276 | */ |
| 277 | private inotifyLimitWarned = false; |
| 278 | /** |
| 279 | * One-way latch: the reason live watching was permanently disabled at runtime |
| 280 | * (watch-resource exhaustion, lock contention past the retry budget, or a |
| 281 | * persistent generic sync failure past the retry budget), or null while |
| 282 | * healthy. Set by {@link degrade}; cleared only by a fresh start(). |
| 283 | */ |
| 284 | private degradedReason: string | null = null; |
| 285 | /** Consecutive lock-contention retries for watcher-triggered syncs. */ |
| 286 | private lockRetryCount = 0; |
| 287 | /** Consecutive generic (non-lock) sync failures; reset only by a clean sync. */ |
| 288 | private syncFailureRetryCount = 0; |
| 289 | /** Test-only inert mode: started, but with no OS watcher installed. */ |
| 290 | private inert = false; |
| 291 | private debounceTimer: ReturnType<typeof setTimeout> | null = null; |
| 292 | /** |
| 293 | * True when the pending set does NOT exactly describe the change (a |
| 294 | * directory removal's children are unknown from the event, #1285) — the |
| 295 | * next sync must be a full scan-diff. Cleared only after a successful FULL |
| 296 | * sync reconciles the tree. |
| 297 | */ |
| 298 | private needsFullScan = false; |
| 299 | /** |
| 300 | * Files seen by the watcher since the last successful sync — populated on |
| 301 | * every change event, cleared at the start of a sync, and re-populated by |
| 302 | * events that arrive mid-sync (or restored on sync failure). Keyed by the |
| 303 | * same project-relative POSIX path the rest of the codebase uses, so a |
| 304 | * caller can intersect tool-response file paths against this map cheaply. |
| 305 | */ |
| 306 | private pendingFiles = new Map<string, { firstSeenMs: number; lastSeenMs: number }>(); |
| 307 | /** |
| 308 | * Wall-clock ms at which the in-flight sync began. Combined with |
| 309 | * {@link pendingFiles}'s `lastSeenMs`, this distinguishes "still in the |
| 310 | * debounce window" (lastSeen > syncStarted, sync hasn't started yet for |
| 311 | * this edit) from "currently being indexed" (lastSeen <= syncStarted). |
| 312 | */ |
| 313 | private syncStartedMs = 0; |
| 314 | private syncing = false; |
| 315 | private stopped = false; |
| 316 | /** |
| 317 | * True once the initial watch set is established. Unlike the previous |
| 318 | * chokidar implementation there is no asynchronous initial "crawl" emitting |
| 319 | * an `add` per existing file — `fs.watch` only reports changes from the |
| 320 | * moment it's installed — so this flips to true synchronously at the end of |
nothing calls this directly
no outgoing calls
no test coverage detected