# 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(),
      });
    }