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
+7 -5
View File
@@ -1,10 +1,9 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Config, BridgeStatus, BridgeStats } from "../types";
const MAX_LOG_LINES = 500;
const POLL_INTERVAL = 1000;
export function useBridge() {
const [config, setConfig] = useState<Config | null>(null);
@@ -18,7 +17,6 @@ export function useBridge() {
const [bridgePassword, setBridgePassword] = useState<string | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const refresh = useCallback(() => {
invoke<BridgeStatus>("get_status").then(setStatus);
@@ -31,9 +29,13 @@ export function useBridge() {
invoke<string | null>("get_bridge_password").then(setBridgePassword);
refresh();
pollRef.current = setInterval(refresh, POLL_INTERVAL);
// The bridge pushes `bridge://stats` and `bridge://status` whenever
// anything changes (mail count, ws state, start/stop). No setInterval.
const unlistenStats = listen<BridgeStats>("bridge://stats", (e) => setStats(e.payload));
const unlistenStatus = listen<BridgeStatus>("bridge://status", (e) => setStatus(e.payload));
return () => {
if (pollRef.current) clearInterval(pollRef.current);
unlistenStats.then((fn) => fn());
unlistenStatus.then((fn) => fn());
};
}, [refresh]);