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 serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
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.