Drop the last two timers: prefetch and the UI stats poll go event-driven

Phase 2.5 — `prefetch_loop` no longer wakes every 30s. It subscribes to
`MailStore::subscribe()` and only catches up missing bodies when the
generation counter ticks (event-bus delta, sync_folder finishing,
bootstrap). When the store is quiet — every cached mail has its .eml on
disk — the loop sleeps indefinitely on the watch channel. The
`PREFETCH_INTERVAL` constant is gone.

Phase 2.6 — the dashboard 1s `setInterval` is gone too. `BridgeHandle`
gets a `stats_dirty_tx: broadcast::Sender<()>` that pulses on every
`MailStore` bump, every `WsState` transition, and the start / stop
status transitions. A small watcher task inside `start()` plumbs the
two `watch::Receiver`s into the broadcast. The Tauri layer
(`stream_stats`) subscribes via `BridgeHandle::subscribe_stats()`,
takes a stats + status snapshot on every pulse (and once at startup),
and emits two events `bridge://stats` / `bridge://status` to the
webview. The React hook replaces `setInterval(refresh, 1000)` with two
`listen()` subscriptions; the initial `refresh()` still seeds the
state for the first frame.

Backend now has zero `time::sleep`-driven polling loops: the syncer is
driven by Phase 0 + bootstrap + the event bus, prefetch is driven by
store changes, and the UI is driven by pushes. The only remaining
delays are throttling (`INTER_FOLDER_DELAY`, `INTER_REQUEST_DELAY`)
and reconnect backoff, which are not polls.

166 bridge lib tests, full workspace builds, UI tsc clean.
This commit is contained in:
Anthony
2026-05-28 15:19:52 +02:00
parent ffc1d7c9d0
commit 9148787497
4 changed files with 95 additions and 10 deletions
+37
View File
@@ -18,6 +18,7 @@ fn main() {
let handle = BridgeHandle::new();
let log_rx = handle.subscribe_logs();
let stats_rx = handle.subscribe_stats();
let shared = Arc::new(Mutex::new(handle));
tauri::Builder::default()
@@ -40,6 +41,12 @@ fn main() {
stream_logs(app_handle, log_rx).await;
});
let app_handle = app.handle().clone();
let stats_state = app.state::<BridgeState>().inner().clone();
tauri::async_runtime::spawn(async move {
stream_stats(app_handle, stats_rx, stats_state).await;
});
let state = app.state::<BridgeState>().inner().clone();
tauri::async_runtime::spawn(async move {
auto_start(state).await;
@@ -75,6 +82,36 @@ async fn auto_start(state: Arc<Mutex<BridgeHandle>>) {
}
}
async fn stream_stats(
app: tauri::AppHandle,
mut rx: tokio::sync::broadcast::Receiver<()>,
state: BridgeState,
) {
use tauri::Emitter;
// Emit a single snapshot covering both bridge status and stats. Status
// transitions (start / stop) and stats changes (new mail, ws state) all
// pulse the same channel, so the UI replaces its periodic poll with one
// listen per topic.
async fn emit_snapshot(app: &tauri::AppHandle, state: &BridgeState) {
let handle = state.lock().await;
let status = handle.status().await;
let stats = handle.stats().await;
drop(handle);
let _ = app.emit("bridge://stats", &stats);
let _ = app.emit("bridge://status", &status);
}
emit_snapshot(&app, &state).await;
loop {
match rx.recv().await {
Ok(()) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
emit_snapshot(&app, &state).await;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
}
async fn stream_logs(
app: tauri::AppHandle,
mut rx: tokio::sync::broadcast::Receiver<String>,