watcher(kqueue): drop per-cycle Vec, two-phase init, and raw libc::kevent
Cleans up src/watcher/KEventWatcher.rs per the Rust-cleanup pass: move guarantees into the type system, no perf regression, no behavior change.
What changed
KEventWatcher.rs (143 -> 92 lines)
fd: Option<Fd>+init(&mut self)->fd: Fd+fn new() -> Result<Self>. The.expect("KEventWatcher has an invalid file descriptor")is gone.- Two
unsafe { c::kevent(...) }blocks -> the existing safebun_sys::keventslice wrapper. The file is nowunsafe-free. As a side effect, akevent()error now propagates toon_errorvia?instead of panicking inusize::try_from(count). - The
.to_vec()that copied the deduped event slice every cycle (added only to end a borrow before callingwrite_trace_events(&self, ...)) is removed. - No direct references to
mutex,ctx,on_file_update, orchanged_filepathsremain; those are accessed via the newdispatch_file_updatesbelow.
Watcher.rs
Watcher::initconstructsPlatform::new(top_level_dir)?directly instead ofPlatform::default()+ in-placeinit().- New
dispatch_file_updates(&mut self, event_count, changed_count): lock ->running.load()-> trace ->on_file_update, using disjoint field borrows so the event slice is passed in place. Every platform backend now calls this instead of open-coding the sequence. add_file_descriptor_to_kqueue_without_checksuses the safebun_sys::keventwrapper andself.platform.fd(no.unwrap()).- Removed the one-line
write_trace_eventswrapper (only caller was the duplicated dispatch block).
INotifyWatcher.rs / WindowsWatcher.rs
- Both now expose
new()(Windows keeps its in-placeinitas a private helper since the struct carries a 64 KiB buffer inline). - Both call
dispatch_file_updates; the Windows backend's per-cycle.to_vec()is gone too.
Preserved semantics
Event coalescing (the 100 us re-poll when count < 64), the dedup/merge pass, and the lock held around on_file_update (which flush_evictions relies on) are unchanged.
Not in this PR
The detached-mutex / fn-pointer-ctx shape now lives in exactly one place (dispatch_file_updates) rather than three, but Watcher.mutex is still a peer field rather than watchlist: Guarded<WatchList>. Wrapping it requires restructuring flush_evictions, which is reached mid-callback via a raw-pointer detour from HotReloader::on_file_update / DevServer::on_file_update while the lock is already held; a Guarded there would deadlock on the non-recursive mutex. That restructure touches the scopeguard drop ordering that keeps the JS-thread EBADF window closed, so it belongs in its own change.
Verification
cargo check -p bun_watcheronaarch64-apple-darwin,x86_64-unknown-freebsd,x86_64-unknown-linux-gnu,aarch64-linux-android,x86_64-pc-windows-msvccargo clippy -p bun_watcher --no-depsclean on darwin/freebsd/linux (windows has pre-existing unrelated lints)bun bd test test/cli/hot/hot.test.ts test/cli/watch/watcher-trace.test.ts test/cli/hot/watch.test.ts test/cli/hot/watch-many-dirs.test.tsall pass on linux (exercises the shareddispatch_file_updatespath via inotify)