155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
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<string, never>;
|
|
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<TWatcherCommands>;
|
|
private _isWatching = false;
|
|
|
|
public readonly events$ = new smartrx.rxjs.Subject<IWatchEvent>();
|
|
|
|
constructor(private options: IWatcherOptions) {
|
|
this.bridge = new smartrust.RustBridge<TWatcherCommands>({
|
|
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<boolean> {
|
|
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<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|
|
}
|