import * as path from 'node:path'; import * as smartrx from '@push.rocks/smartrx'; import * as smartrust from '@push.rocks/smartrust'; import type { IWatcher, IWatcherOptions, IWatchEvent, TWatchEventType } from './interfaces.js'; // Resolve the package directory for binary location const packageDir = path.resolve(new URL('.', import.meta.url).pathname, '..', '..'); /** * Command map for the Rust file watcher binary */ type TWatcherCommands = { watch: { params: { paths: string[]; depth: number; followSymlinks: boolean; debounceMs: number; }; result: { watching: boolean }; }; stop: { params: Record; result: { stopped: boolean }; }; }; /** * Build local search paths for the Rust binary */ function buildLocalPaths(): string[] { const platform = process.platform === 'darwin' ? 'macos' : process.platform; const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch; const platformSuffix = `${platform}_${arch}`; return [ path.join(packageDir, 'dist_rust', `smartwatch-rust_${platformSuffix}`), path.join(packageDir, 'dist_rust', 'smartwatch-rust'), path.join(packageDir, 'rust', 'target', 'release', 'smartwatch-rust'), path.join(packageDir, 'rust', 'target', 'debug', 'smartwatch-rust'), ]; } /** * Rust-based file watcher using the notify crate via @push.rocks/smartrust * * Uses a Rust binary for native OS-level file watching (inotify/FSEvents/ReadDirectoryChangesW). * Works across Node.js, Deno, and Bun via smartrust's IPC bridge. */ export class RustWatcher implements IWatcher { private bridge: smartrust.RustBridge; private _isWatching = false; public readonly events$ = new smartrx.rxjs.Subject(); constructor(private options: IWatcherOptions) { this.bridge = new smartrust.RustBridge({ binaryName: 'smartwatch-rust', localPaths: buildLocalPaths(), searchSystemPath: false, cliArgs: ['--management'], requestTimeoutMs: 30000, readyTimeoutMs: 10000, }); } get isWatching(): boolean { return this._isWatching; } /** * Check if the Rust binary is available on this system */ static async isAvailable(): Promise { try { const locator = new smartrust.RustBinaryLocator({ binaryName: 'smartwatch-rust', localPaths: buildLocalPaths(), searchSystemPath: false, }); const binaryPath = await locator.findBinary(); return binaryPath !== null; } catch { return false; } } async start(): Promise { if (this._isWatching) return; console.log(`[smartwatch] Starting Rust watcher for ${this.options.basePaths.length} base path(s)...`); // Listen for file system events from the Rust binary this.bridge.on('management:fsEvent', (data: { type: string; path: string }) => { const eventType = data.type as TWatchEventType; this.safeEmit({ type: eventType, path: data.path }); }); this.bridge.on('management:error', (data: { message: string }) => { console.error('[smartwatch] Rust watcher error:', data.message); this.safeEmit({ type: 'error', path: '', error: new Error(data.message) }); }); this.bridge.on('management:watchReady', () => { console.log('[smartwatch] Rust watcher ready - initial scan complete'); this.safeEmit({ type: 'ready', path: '' }); }); // Spawn the Rust binary const ok = await this.bridge.spawn(); if (!ok) { throw new Error('[smartwatch] Failed to spawn Rust watcher binary'); } // Resolve paths to absolute const absolutePaths = this.options.basePaths.map(p => path.resolve(p)); // Send watch command await this.bridge.sendCommand('watch', { paths: absolutePaths, depth: this.options.depth, followSymlinks: this.options.followSymlinks, debounceMs: this.options.debounceMs, }); this._isWatching = true; console.log('[smartwatch] Rust watcher started'); } async stop(): Promise { console.log('[smartwatch] Stopping Rust watcher...'); if (this._isWatching) { try { await this.bridge.sendCommand('stop', {} as any); } catch { // Binary may already be gone } } this.bridge.kill(); this._isWatching = false; console.log('[smartwatch] Rust 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); } } }