import * as fs from 'fs'; import * as path from 'path'; import * as smartrx from '@push.rocks/smartrx'; import * as chokidar from 'chokidar'; import type { IWatcher, IWatcherOptions, IWatchEvent } from './interfaces.js'; /** * Node.js/Bun file watcher using chokidar * * Chokidar handles all the edge cases: * - Atomic writes (temp file + rename) * - Inode tracking * - Cross-platform differences * - Debouncing * - Write stabilization */ export class NodeWatcher implements IWatcher { private watcher: chokidar.FSWatcher | null = null; private _isWatching = false; public readonly events$ = new smartrx.rxjs.Subject(); constructor(private options: IWatcherOptions) {} get isWatching(): boolean { return this._isWatching; } async start(): Promise { if (this._isWatching) return; console.log(`[smartwatch] Starting chokidar watcher for ${this.options.basePaths.length} base path(s)...`); try { // Resolve all paths to absolute const absolutePaths = this.options.basePaths.map(p => path.resolve(p)); this.watcher = chokidar.watch(absolutePaths, { persistent: true, ignoreInitial: false, followSymlinks: this.options.followSymlinks, depth: this.options.depth, atomic: true, // Handle atomic writes awaitWriteFinish: this.options.awaitWriteFinish ? { stabilityThreshold: this.options.stabilityThreshold || 300, pollInterval: this.options.pollInterval || 100, } : false, }); // Wire up all events this.watcher .on('add', (filePath: string, stats?: fs.Stats) => { this.safeEmit({ type: 'add', path: filePath, stats }); }) .on('change', (filePath: string, stats?: fs.Stats) => { this.safeEmit({ type: 'change', path: filePath, stats }); }) .on('unlink', (filePath: string) => { this.safeEmit({ type: 'unlink', path: filePath }); }) .on('addDir', (filePath: string, stats?: fs.Stats) => { this.safeEmit({ type: 'addDir', path: filePath, stats }); }) .on('unlinkDir', (filePath: string) => { this.safeEmit({ type: 'unlinkDir', path: filePath }); }) .on('error', (error: Error) => { console.error('[smartwatch] Chokidar error:', error); this.safeEmit({ type: 'error', path: '', error }); }) .on('ready', () => { console.log('[smartwatch] Chokidar ready - initial scan complete'); this.safeEmit({ type: 'ready', path: '' }); }); this._isWatching = true; console.log('[smartwatch] Watcher started'); } catch (error: any) { console.error('[smartwatch] Failed to start watcher:', error); this.safeEmit({ type: 'error', path: '', error }); throw error; } } async stop(): Promise { console.log('[smartwatch] Stopping watcher...'); if (this.watcher) { await this.watcher.close(); this.watcher = null; } this._isWatching = false; console.log('[smartwatch] Watcher stopped'); } /** Safely emit an event, isolating subscriber errors */ private safeEmit(event: IWatchEvent): void { try { this.events$.next(event); } catch (error) { console.error('[smartwatch] Subscriber threw error (isolated):', error); } } }