All files / src/core/file-system watch-api-dir.ts

29.72% Statements 11/37
26.08% Branches 6/23
50% Functions 3/6
32.35% Lines 11/34

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                                                                3x 1x     2x 3x 3x   3x                       3x                   3x                                           3x   2x 2x          
/**
 * API directory watcher
 * Core logic - abstracted to be runtime agnostic
 */
 
import * as fs from 'fs';
import * as path from 'path';
import { ROUTE_FILE_PATTERN, MIDDLEWARE_FILE_PATTERN } from '../../shared/constants.js';
 
export type FileChangeEvent = 'add' | 'change' | 'unlink';
export type FileChangeCallback = (event: FileChangeEvent, filePath: string) => void;
 
export interface WatchApiDirectoryOptions {
  debounceMs?: number;
}
 
/**
 * Interface for watcher (allows different implementations)
 */
export interface ApiWatcher {
  close(): void;
}
 
/**
 * Creates a watcher for the API directory using Node.js fs.watch
 * Returns a function to stop watching
 */
export function watchApiDirectory(
  apiDir: string,
  callback: FileChangeCallback,
  options: WatchApiDirectoryOptions = {}
): ApiWatcher {
  if (!fs.existsSync(apiDir)) {
    return { close: () => {} };
  }
 
  const debounceMs = options.debounceMs ?? 0;
  let pending: Array<{ event: FileChangeEvent; filePath: string }> = [];
  let timer: ReturnType<typeof setTimeout> | null = null;
 
  const flush = () => {
    if (timer != null) {
      clearTimeout(timer);
      timer = null;
    }
    const toEmit = pending;
    pending = [];
    for (const { event, filePath } of toEmit) {
      callback(event, filePath);
    }
  };
 
  const schedule = (event: FileChangeEvent, filePath: string) => {
    pending.push({ event, filePath });
    if (debounceMs <= 0) {
      flush();
      return;
    }
    if (timer != null) clearTimeout(timer);
    timer = setTimeout(flush, debounceMs);
  };
 
  const watcher = fs.watch(apiDir, { recursive: true }, (eventType, filename) => {
    if (!filename) return;
 
    const filePath = path.join(apiDir, filename);
 
    const isRouteFile = ROUTE_FILE_PATTERN.test(filename);
    const isMiddlewareFile = MIDDLEWARE_FILE_PATTERN.test(filename);
 
    if (!isRouteFile && !isMiddlewareFile) {
      return;
    }
 
    let normalizedEvent: FileChangeEvent;
    if (eventType === 'rename') {
      normalizedEvent = fs.existsSync(filePath) ? 'add' : 'unlink';
    } else {
      normalizedEvent = eventType as FileChangeEvent;
    }
 
    schedule(normalizedEvent, filePath);
  });
 
  return {
    close: () => {
      Iif (timer != null) clearTimeout(timer);
      watcher.close();
    },
  };
}