/** * Watcher builder for file system watching */ import type { ISmartFsProvider, IWatcherHandle } from '../interfaces/mod.provider.js'; import type { IWatchEvent, IWatchOptions, TWatchEventType } from '../interfaces/mod.types.js'; /** * Event handler type */ type TEventHandler = (event: IWatchEvent) => void | Promise; /** * Active watcher handle that allows stopping the watcher */ export class SmartFsActiveWatcher { constructor(private handle: IWatcherHandle) {} /** * Stop watching for file system changes */ public async stop(): Promise { return this.handle.stop(); } } /** * Watcher builder class for file system watching * Configuration methods return `this` for chaining * Call `.start()` to begin watching */ export class SmartFsWatcher { private provider: ISmartFsProvider; private path: string; // Configuration options private options: { recursive?: boolean; filter?: string | RegExp | ((path: string) => boolean); debounce?: number; } = {}; // Event handlers private handlers: { change?: TEventHandler[]; add?: TEventHandler[]; delete?: TEventHandler[]; all?: TEventHandler[]; } = {}; // Debounce state private debounceTimers: Map = new Map(); constructor(provider: ISmartFsProvider, path: string) { this.provider = provider; this.path = this.provider.normalizePath(path); if (!this.provider.capabilities.supportsWatch) { throw new Error(`Provider '${this.provider.name}' does not support file watching`); } } // --- Configuration Methods (return this for chaining) --- /** * Enable recursive watching (watch subdirectories) */ public recursive(): this { this.options.recursive = true; return this; } /** * Filter watched paths * @param filter - String pattern, RegExp, or filter function * * @example * ```typescript * // String pattern (glob-like) * .filter('*.ts') * * // RegExp * .filter(/\.ts$/) * * // Function * .filter(path => path.endsWith('.ts')) * ``` */ public filter(filter: string | RegExp | ((path: string) => boolean)): this { this.options.filter = filter; return this; } /** * Debounce events (wait N milliseconds before firing) * Useful for avoiding rapid-fire events * @param ms - Debounce delay in milliseconds */ public debounce(ms: number): this { this.options.debounce = ms; return this; } // --- Event Handler Registration (return this for chaining) --- /** * Register handler for 'change' events (file modified) * @param handler - Event handler function */ public onChange(handler: TEventHandler): this { if (!this.handlers.change) { this.handlers.change = []; } this.handlers.change.push(handler); return this; } /** * Register handler for 'add' events (file created) * @param handler - Event handler function */ public onAdd(handler: TEventHandler): this { if (!this.handlers.add) { this.handlers.add = []; } this.handlers.add.push(handler); return this; } /** * Register handler for 'delete' events (file deleted) * @param handler - Event handler function */ public onDelete(handler: TEventHandler): this { if (!this.handlers.delete) { this.handlers.delete = []; } this.handlers.delete.push(handler); return this; } /** * Register handler for all events * @param handler - Event handler function */ public onAll(handler: TEventHandler): this { if (!this.handlers.all) { this.handlers.all = []; } this.handlers.all.push(handler); return this; } // --- Action Method --- /** * Start watching for file system changes * @returns Active watcher handle that can be stopped */ public async start(): Promise { const watchOptions: IWatchOptions = { recursive: this.options.recursive, filter: this.options.filter, debounce: this.options.debounce, }; // Create the callback that dispatches to handlers const callback = async (event: IWatchEvent) => { await this.handleEvent(event); }; const handle = await this.provider.watch(this.path, callback, watchOptions); return new SmartFsActiveWatcher(handle); } /** * Handle incoming watch events (internal) */ private async handleEvent(event: IWatchEvent): Promise { // Apply debouncing if configured if (this.options.debounce && this.options.debounce > 0) { const key = `${event.type}:${event.path}`; // Clear existing timer const existingTimer = this.debounceTimers.get(key); if (existingTimer) { clearTimeout(existingTimer); } // Set new timer const timer = setTimeout(async () => { this.debounceTimers.delete(key); await this.dispatchEvent(event); }, this.options.debounce); this.debounceTimers.set(key, timer); } else { // No debouncing, dispatch immediately await this.dispatchEvent(event); } } /** * Dispatch event to registered handlers (internal) */ private async dispatchEvent(event: IWatchEvent): Promise { // Dispatch to type-specific handlers const typeHandlers = this.handlers[event.type]; if (typeHandlers) { for (const handler of typeHandlers) { await handler(event); } } // Dispatch to 'all' handlers if (this.handlers.all) { for (const handler of this.handlers.all) { await handler(event); } } } /** * Get the watched path */ public getPath(): string { return this.path; } }