All files / backend/src/services/workspace model-watcher.ts

89.47% Statements 17/19
100% Branches 2/2
71.42% Functions 5/7
89.47% Lines 17/19

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                                                                  10x 10x 10x               10x   10x   10x           10x   10x 9x 9x     10x                   12x 2x 2x                                             10x 10x 10x    
/**
 * Model Watcher
 *
 * Watches the quiqr/model directory for changes and clears the workspace
 * configuration cache when files are added, changed, or removed.
 */
 
import chokidar, { type FSWatcher, type ChokidarOptions } from 'chokidar';
import path from 'path';
import type { WorkspaceConfigProvider } from './workspace-config-provider.js';
 
/**
 * Options for creating a ModelWatcher
 */
export interface ModelWatcherOptions {
  workspacePath: string;
  workspaceConfigProvider: WorkspaceConfigProvider;
  onCacheCleared?: () => void; // Optional callback for debugging/testing
}
 
/**
 * ModelWatcher - Watches model directory for changes
 *
 * When any file in {workspacePath}/quiqr/model changes,
 * it clears the WorkspaceConfigProvider cache.
 */
export class ModelWatcher {
  private watcher: FSWatcher | undefined;
  private workspacePath: string;
  private workspaceConfigProvider: WorkspaceConfigProvider;
  private onCacheCleared?: () => void;
 
  constructor(options: ModelWatcherOptions) {
    this.workspacePath = options.workspacePath;
    this.workspaceConfigProvider = options.workspaceConfigProvider;
    this.onCacheCleared = options.onCacheCleared;
  }
 
  /**
   * Start watching the model directory
   */
  start(): void {
    // Stop any existing watcher first
    this.stop();
 
    const watchDir = path.join(this.workspacePath, 'quiqr', 'model');
 
    const watchOptions: ChokidarOptions = {
      ignored: /(^|[/\\])\../, // ignore dotfiles
      persistent: true,
      ignoreInitial: true, // Don't fire events for existing files on startup
    };
 
    this.watcher = chokidar.watch(watchDir, watchOptions);
 
    const handleChange = () => {
      this.workspaceConfigProvider.clearCache();
      this.onCacheCleared?.();
    };
 
    this.watcher
      .on('add', handleChange)
      .on('change', handleChange)
      .on('unlink', handleChange);
  }
 
  /**
   * Stop watching and clean up resources
   */
  async stop(): Promise<void> {
    if (this.watcher) {
      await this.watcher.close();
      this.watcher = undefined;
    }
  }
 
  /**
   * Check if the watcher is currently active
   */
  isWatching(): boolean {
    return this.watcher !== undefined;
  }
 
  /**
   * Get the path being watched
   */
  getWatchPath(): string {
    return path.join(this.workspacePath, 'quiqr', 'model');
  }
}
 
/**
 * Factory function to create and start a ModelWatcher
 */
export function createModelWatcher(options: ModelWatcherOptions): ModelWatcher {
  const watcher = new ModelWatcher(options);
  watcher.start();
  return watcher;
}