feat(watchers): add Rust-powered watcher backend with runtime fallback and cross-platform test coverage

This commit is contained in:
2026-03-23 14:15:31 +00:00
parent ca9a66e03e
commit 7def7020c6
26 changed files with 10383 additions and 2870 deletions

View File

@@ -17,11 +17,19 @@ import type { IWatcher, IWatcherOptions, IWatchEvent } from './interfaces.js';
export class NodeWatcher implements IWatcher {
private watcher: chokidar.FSWatcher | null = null;
private _isWatching = false;
private _preExistingHandles: Set<any> = new Set();
public readonly events$ = new smartrx.rxjs.Subject<IWatchEvent>();
constructor(private options: IWatcherOptions) {}
/** Collect all current FSWatcher handles from the process */
private _getFsWatcherHandles(): any[] {
return (process as any)._getActiveHandles().filter(
(h: any) => h?.constructor?.name === 'FSWatcher' && typeof h.unref === 'function'
);
}
get isWatching(): boolean {
return this._isWatching;
}
@@ -29,6 +37,9 @@ export class NodeWatcher implements IWatcher {
async start(): Promise<void> {
if (this._isWatching) return;
// Snapshot existing FSWatcher handles so we only unref ours on stop
this._preExistingHandles = new Set(this._getFsWatcherHandles());
console.log(`[smartwatch] Starting chokidar watcher for ${this.options.basePaths.length} base path(s)...`);
try {
@@ -90,13 +101,16 @@ export class NodeWatcher implements IWatcher {
this.watcher = null;
}
// Unref any lingering FSWatcher handles from chokidar so they don't prevent process exit.
// Chokidar v5's close() resolves before all fs.watch() handles are fully released.
for (const handle of (process as any)._getActiveHandles()) {
if (handle?.constructor?.name === 'FSWatcher' && typeof handle.unref === 'function') {
// Unref only FSWatcher handles created during our watch session.
// Chokidar v5 can orphan fs.watch() handles under heavy file churn,
// preventing process exit. We only touch handles that didn't exist
// before start() to avoid affecting other watchers in the process.
for (const handle of this._getFsWatcherHandles()) {
if (!this._preExistingHandles.has(handle)) {
handle.unref();
}
}
this._preExistingHandles.clear();
this._isWatching = false;
console.log('[smartwatch] Watcher stopped');