Skip to content

Commit 500acb7

Browse files
committed
fix: use trailing-edge debounce for file watcher
1 parent 62ba417 commit 500acb7

2 files changed

Lines changed: 21 additions & 39 deletions

File tree

src/main.rs

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use hyper::service::service_fn;
2222
use hyper_util::rt::{TokioExecutor, TokioIo};
2323
use hyper_util::server::conn::auto::Builder as HttpConnectionBuilder;
2424
use hyper_util::server::graceful::GracefulShutdown;
25-
use notify::{RecursiveMode, Watcher};
25+
use notify::RecursiveMode;
2626
use tokio::signal;
2727
use tokio::sync::mpsc;
2828

@@ -154,50 +154,27 @@ fn spawn_file_watcher(script_path: PathBuf, tx: mpsc::Sender<String>) {
154154

155155
let (raw_tx, raw_rx) = std::sync::mpsc::channel();
156156

157-
let mut watcher = notify::recommended_watcher(raw_tx).expect("Failed to create watcher");
157+
let mut debouncer =
158+
notify_debouncer_mini::new_debouncer(Duration::from_millis(100), raw_tx)
159+
.expect("Failed to create debouncer");
158160

159-
watcher
161+
debouncer
162+
.watcher()
160163
.watch(&watch_dir, RecursiveMode::Recursive)
161164
.expect("Failed to watch directory");
162165

163-
// Keep watcher alive
164-
let _watcher = watcher;
165-
166-
// Set to past time so first event isn't debounced
167-
let mut last_reload = std::time::Instant::now() - Duration::from_secs(1);
168-
let debounce = Duration::from_millis(100);
169-
170166
for result in raw_rx {
171167
match result {
172-
Ok(event) => {
173-
// Only react to modifications, not access/open events
174-
use notify::EventKind;
175-
let is_modification = matches!(
176-
event.kind,
177-
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
178-
);
179-
if !is_modification {
180-
continue;
181-
}
182-
183-
// Debounce rapid events
184-
if last_reload.elapsed() < debounce {
185-
continue;
186-
}
187-
last_reload = std::time::Instant::now();
188-
189-
// Re-read and send the script file
190-
match std::fs::read_to_string(&script_path) {
191-
Ok(content) => {
192-
if tx.blocking_send(content).is_err() {
193-
break;
194-
}
195-
}
196-
Err(e) => {
197-
eprintln!("Error reading script file: {e}");
168+
Ok(_events) => match std::fs::read_to_string(&script_path) {
169+
Ok(content) => {
170+
if tx.blocking_send(content).is_err() {
171+
break;
198172
}
199173
}
200-
}
174+
Err(e) => {
175+
eprintln!("Error reading script file: {e}");
176+
}
177+
},
201178
Err(e) => {
202179
eprintln!("Watch error: {e:?}");
203180
}

tests/server_test.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,10 +1435,15 @@ async fn test_watch_file_reload_on_change() {
14351435
.expect("curl failed");
14361436
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "version1");
14371437

1438-
// Modify the script file
1438+
// Trigger a spurious event, then write the actual change.
1439+
// This tests trailing-edge debounce: the reload should wait for events to
1440+
// settle and read the final content, not the content at the first event.
1441+
let dummy_path = tmp.path().join("trigger.txt");
1442+
std::fs::write(&dummy_path, "trigger").unwrap();
1443+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
14391444
std::fs::write(&script_path, r#"{|req| "version2"}"#).unwrap();
14401445

1441-
// Wait for file watcher to detect change and reload
1446+
// Wait for debounced reload to complete
14421447
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
14431448

14441449
// Verify updated response

0 commit comments

Comments
 (0)