2025-11-30 03:04:49 +00:00
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import * as smartrx from '@push.rocks/smartrx';
|
2025-12-11 21:04:42 +00:00
|
|
|
import * as chokidar from 'chokidar';
|
|
|
|
|
import type { IWatcher, IWatcherOptions, IWatchEvent } from './interfaces.js';
|
2025-11-30 03:04:49 +00:00
|
|
|
|
2025-12-11 11:35:45 +00:00
|
|
|
/**
|
2025-12-11 21:04:42 +00:00
|
|
|
* Node.js/Bun file watcher using chokidar
|
2025-12-11 11:35:45 +00:00
|
|
|
*
|
2025-12-11 21:04:42 +00:00
|
|
|
* Chokidar handles all the edge cases:
|
|
|
|
|
* - Atomic writes (temp file + rename)
|
|
|
|
|
* - Inode tracking
|
|
|
|
|
* - Cross-platform differences
|
|
|
|
|
* - Debouncing
|
|
|
|
|
* - Write stabilization
|
2025-12-11 11:35:45 +00:00
|
|
|
*/
|
|
|
|
|
export class NodeWatcher implements IWatcher {
|
2025-12-11 21:04:42 +00:00
|
|
|
private watcher: chokidar.FSWatcher | null = null;
|
2025-12-11 11:35:45 +00:00
|
|
|
private _isWatching = false;
|
2025-12-10 14:18:40 +00:00
|
|
|
|
2025-12-11 11:35:45 +00:00
|
|
|
public readonly events$ = new smartrx.rxjs.Subject<IWatchEvent>();
|
2025-12-08 17:48:50 +00:00
|
|
|
|
2025-12-11 19:13:35 +00:00
|
|
|
constructor(private options: IWatcherOptions) {}
|
2025-12-11 09:07:57 +00:00
|
|
|
|
2025-11-30 03:04:49 +00:00
|
|
|
get isWatching(): boolean {
|
|
|
|
|
return this._isWatching;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
2025-12-11 11:35:45 +00:00
|
|
|
if (this._isWatching) return;
|
2025-11-30 03:04:49 +00:00
|
|
|
|
2025-12-11 21:04:42 +00:00
|
|
|
console.log(`[smartwatch] Starting chokidar watcher for ${this.options.basePaths.length} base path(s)...`);
|
2025-12-08 17:48:50 +00:00
|
|
|
|
2025-11-30 03:04:49 +00:00
|
|
|
try {
|
2025-12-11 21:04:42 +00:00
|
|
|
// 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,
|
|
|
|
|
});
|
2025-12-11 02:39:38 +00:00
|
|
|
|
2025-12-11 21:04:42 +00:00
|
|
|
// 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: '' });
|
|
|
|
|
});
|
2025-11-30 03:04:49 +00:00
|
|
|
|
|
|
|
|
this._isWatching = true;
|
2025-12-11 21:04:42 +00:00
|
|
|
console.log('[smartwatch] Watcher started');
|
2025-11-30 03:04:49 +00:00
|
|
|
} catch (error: any) {
|
2025-12-08 17:48:50 +00:00
|
|
|
console.error('[smartwatch] Failed to start watcher:', error);
|
2025-12-11 21:04:42 +00:00
|
|
|
this.safeEmit({ type: 'error', path: '', error });
|
2025-11-30 03:04:49 +00:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async stop(): Promise<void> {
|
2025-12-08 17:48:50 +00:00
|
|
|
console.log('[smartwatch] Stopping watcher...');
|
2025-12-11 02:39:38 +00:00
|
|
|
|
2025-12-11 21:04:42 +00:00
|
|
|
if (this.watcher) {
|
|
|
|
|
await this.watcher.close();
|
|
|
|
|
this.watcher = null;
|
2025-12-11 11:35:45 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 03:04:49 +00:00
|
|
|
this._isWatching = false;
|
2025-12-08 17:48:50 +00:00
|
|
|
console.log('[smartwatch] Watcher stopped');
|
2025-11-30 03:04:49 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-11 11:35:45 +00:00
|
|
|
/** 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-30 03:04:49 +00:00
|
|
|
}
|