rust
57 lines · 7 steps
Debouncing filesystem events in async Rust
A watcher collects rapid file changes and flushes them in one batch once the churn goes quiet.
Explained by
highlit
1use std::collections::HashSet;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
6use tokio::sync::mpsc;
7use tokio::time::{sleep, Instant};
8
9pub async fn watch_debounced(
10 root: PathBuf,
11 window: Duration,
12 mut on_flush: impl FnMut(HashSet<PathBuf>),
13) -> notify::Result<()> {
14 let (tx, mut rx) = mpsc::unbounded_channel::<Event>();
15
16 let mut watcher = RecommendedWatcher::new(
17 move |res: notify::Result<Event>| {
18 if let Ok(event) = res {
19 let _ = tx.send(event);
20 }
21 },
22 notify::Config::default(),
23 )?;
24 watcher.watch(&root, RecursiveMode::Recursive)?;
25
26 let mut pending: HashSet<PathBuf> = HashSet::new();
27 let mut deadline: Option<Instant> = None;
28
29 loop {
30 let tick = async {
31 match deadline {
32 Some(at) => sleep(at.saturating_duration_since(Instant::now())).await,
33 None => std::future::pending().await,
34 }
35 };
36
37 tokio::select! {
38 maybe_event = rx.recv() => {
39 match maybe_event {
40 Some(event) => {
41 pending.extend(event.paths);
42 deadline = Some(Instant::now() + window);
43 }
44 None => break,
45 }
46 }
47 _ = tick => {
48 if !pending.is_empty() {
49 on_flush(std::mem::take(&mut pending));
50 }
51 deadline = None;
52 }
53 }
54 }
55
56 Ok(())
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Debouncing turns a storm of events into a single batch by resetting a deadline on every new arrival.
- 2A pending future lets tokio::select! wait indefinitely when there is no timer to race against.
- 3Bridging a callback-based API to async is a matter of forwarding events through a channel.
Related explainers
rust
use std::cmp::Ordering; pub struct PrefixIndex { entries: Vec<String>,
Prefix search with binary partitioning in Rust
binary-search
sorting
case-insensitive
Intermediate
7 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void; export function observeResize(callback: ResizeCallback, delay = 150) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
Debouncing window resize in TypeScript
debounce
closures
event-listeners
Intermediate
6 steps
rust
use axum::{ extract::State, routing::{get, post, MethodRouter}, Json, Router,
Self-documenting routes in Axum
builder-pattern
closures
shared-state
Intermediate
8 steps
rust
use std::collections::HashMap; use std::fs::File; use std::io::{self, BufRead, BufReader}; use std::path::Path;
Parsing INI files in Rust
parsing
file-io
hashmap
Intermediate
9 steps
rust
use axum::{ extract::{Path, Query}, http::Uri, response::{IntoResponse, Redirect},
Building HTTP redirect handlers in Axum
redirects
extractors
url-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/debouncing-filesystem-events-in-async-rust-explained-rust-6c33/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.