2 releases
Uses new Rust 2024
| 0.0.2 | May 13, 2026 |
|---|---|
| 0.0.1 | May 9, 2026 |
#1557 in Asynchronous
Used in vaned
16KB
159 lines
Notify Two-Phase
A two-phase wrapper around notify-debouncer-full that closes the
"startup gap" between server-bind and fs-watcher-subscribe.
If you bind a listener first and notify::Watcher::watch afterwards,
any file change that lands in the gap is lost — at least on macOS
FSEvents, which does not replay events for files already present at
subscription time. The fix is to subscribe first, queue events
into an unbounded channel during the bind window, and drain them once
the rest of the daemon is up. This crate does that for you.
Two phases
- Phase 1 —
armruns the synchronousnew_debouncer+debouncer.watch()calls and returns aSubscription. From this point on, events are flowing into an unbounded mpsc. - Phase 2 — your async loop awaits
Subscription::recv. Pair it with atokio_util::sync::CancellationTokenfor graceful shutdown. TheSubscriptionis dropped at the end of the loop; that stops the underlying watcher thread.
Example
use std::time::Duration;
use notify_twophase::Subscription;
use tokio_util::sync::CancellationToken;
# async fn run(config_dir: std::path::PathBuf, cancel: CancellationToken) -> Result<(), Box<dyn std::error::Error>> {
let mut sub = notify_twophase::arm(config_dir.clone())?;
// ... bind listeners here; events that land in this window queue ...
tokio::spawn(async move {
loop {
tokio::select! {
biased;
() = cancel.cancelled() => return,
evt = sub.recv() => {
if evt.is_none() { return; }
// reload your config, reconcile listeners, etc.
}
}
}
});
# Ok(())
# }
Default reload filter
arm uses a built-in batch predicate (is_reloadable_batch) that
fires on Create / Modify(Data) / Modify(Name) / Remove events
under the watched tree, and drops Access / Modify(Metadata) /
Other / Any. Use arm_with for a custom debounce window or a
custom predicate.
License
Released under the MIT License © 2026 Canmi
Dependencies
~2–13MB
~82K SLoC