All files / src watcher.ts

12.5% Statements 5/40
20% Branches 2/10
25% Functions 2/8
13.51% Lines 5/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102                        3x                                                       163x   163x 163x                                                   163x                                                                
import chokidar from 'chokidar';
import { relative } from 'path';
import { addFileChange } from './db';
 
// Session-scoped watchers and deduplication
interface WatcherSession {
  watcher: chokidar.FSWatcher;
  changedFiles: Set<string>;
  cwd: string;
  timeouts: Set<NodeJS.Timeout>;
}
 
const sessions = new Map<number, WatcherSession>();
 
export function startWatcher(sessionId: number, cwd: string): void {
  // Don't start duplicate watcher for same session
  if (sessions.has(sessionId)) return;
 
  const watcher = chokidar.watch(cwd, {
    ignored: /(^|[\/\\])\..|(node_modules|dist|build|\.git)/,
    persistent: true,
    ignoreInitial: true,
  });
 
  const session: WatcherSession = {
    watcher,
    changedFiles: new Set<string>(),
    cwd,
    timeouts: new Set<NodeJS.Timeout>(),
  };
 
  sessions.set(sessionId, session);
 
  watcher
    .on('add', (path) => handleChange(sessionId, path, cwd, 'created'))
    .on('change', (path) => handleChange(sessionId, path, cwd, 'modified'))
    .on('unlink', (path) => handleChange(sessionId, path, cwd, 'deleted'));
}
 
export function stopWatcher(sessionId?: number): void {
  if (sessionId !== undefined) {
    // Stop specific session watcher
    const session = sessions.get(sessionId);
    Iif (session) {
      session.watcher.close();
      session.changedFiles.clear();
      // Clear all pending timeouts to prevent leaks
      for (const timeout of session.timeouts) {
        clearTimeout(timeout);
      }
      session.timeouts.clear();
      sessions.delete(sessionId);
    }
  } else E{
    // Legacy: stop all watchers (for backwards compatibility)
    for (const [id, session] of sessions.entries()) {
      session.watcher.close();
      session.changedFiles.clear();
      // Clear all pending timeouts to prevent leaks
      for (const timeout of session.timeouts) {
        clearTimeout(timeout);
      }
      session.timeouts.clear();
      sessions.delete(id);
    }
  }
}
 
export function cleanupWatcher(sessionId: number): void {
  stopWatcher(sessionId);
}
 
function handleChange(
  sessionId: number,
  path: string,
  cwd: string,
  changeType: 'created' | 'modified' | 'deleted'
): void {
  const session = sessions.get(sessionId);
  if (!session) return;
 
  const relativePath = relative(cwd, path);
 
  // Deduplicate rapid changes to same file
  const key = `${relativePath}-${changeType}`;
  if (session.changedFiles.has(key)) return;
 
  session.changedFiles.add(key);
  const timeout = setTimeout(() => {
    session.changedFiles.delete(key);
    session.timeouts.delete(timeout);
  }, 3000);
  session.timeouts.add(timeout);
 
  addFileChange({
    sessionId,
    filePath: relativePath,
    changeType,
    timestamp: new Date().toISOString(),
  });
}