The offline/online switch was janky because two independent systems decided "online" and never communicated: - ConnectivityMonitor owned is_server_reachable (drove the UI banner) but learned reachability only from a standalone /System/Info/Public ping loop and from auth/login calls. - HybridRepository served all real data by racing cache-vs-server but never read or wrote reachability. So the banner reflected a side-channel poller, not the system the user actually experienced: a successful ping could read "online" while authenticated data calls 401'd or timed out, and three different timeout regimes (5s ping / 30s data / 100ms cache race) flapped against each other. Unify into a single source of truth: - Extract a cheap, cloneable ConnectivityReporter that owns all reachability transitions and event emission. - OnlineRepository reports the outcome of every server request to the reporter, classified via RepoError: Ok/Authentication/NotFound/Server => reachable (the server answered), Network => offline candidate, Database/Offline => ignored (not a server signal). - Time-window debounce (OFFLINE_CONFIRM_WINDOW = 5s): flip offline only after sustained network failure; recover instantly on the first success. - Demote the ping loop to an offline-only recovery probe (no online polling; real traffic is the signal when online). - Frontend: navigator.onLine is now advisory (triggers a recheck instead of forcing offline); removed the dead markReachable/markUnreachable store methods. Docs updated (README, 07-connectivity, 03-data-flow, 02-svelte-frontend) to describe the new model and fix pre-existing drift (HTTP client is 30s timeout + 5s ping, not the documented 10s/base_url). Tests: 12 connectivity tests (debounce, instant recovery, RepoError classification through report_outcome). Full suite: 398 Rust + 384 frontend passing, svelte-check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.4 KiB
Connectivity & Network Architecture
HTTP Client with Retry Logic
Location: src-tauri/src/jellyfin/http_client.rs
The HTTP client provides automatic retry with exponential backoff for network resilience:
pub struct HttpClient {
client: reqwest::Client,
config: HttpConfig,
}
pub struct HttpConfig {
pub timeout: Duration, // Default: 30s (large library queries can be slow)
pub max_retries: u32, // Default: 3
}
Note: ordinary requests use the 30s timeout above. The connectivity recovery probe (
ping) uses a shorter, dedicated 5s timeout so an unreachable server is detected quickly while offline.
Retry Strategy:
- Retry delays: 1s, 2s, 4s (exponential backoff)
- Retries on: Network errors, 5xx server errors
- No retry on: 4xx client errors, 401/403 authentication errors
Error Classification:
pub enum ErrorKind {
Network, // Connection failures, timeouts, DNS errors
Authentication, // 401/403 responses
Server, // 5xx server errors
Client, // Other 4xx errors
}
Connectivity Monitor
Location: src-tauri/src/connectivity/mod.rs
The connectivity monitor is the single source of truth for server reachability. Its primary signal is the outcome of real repository traffic — every server request the user actually makes. A standalone /System/Info/Public probe is kept only as an offline recovery detector.
Source of truth: repository traffic
OnlineRepository reports the result of each server request to the monitor, classified via RepoError:
| Repository outcome | Meaning | Effect on reachability |
|---|---|---|
Ok(_) |
Server answered successfully | Mark reachable (instant recovery) |
Err(Authentication) |
Server answered with 401/403 | Mark reachable (server is up; request was rejected) |
Err(NotFound) |
Server answered with 404 | Mark reachable (server is up) |
Err(Server) |
Server answered with 5xx / bad body | Mark reachable (server is up) |
Err(Network) |
Connection failure / timeout / DNS | Candidate for offline (see debounce) |
Err(Database) |
Local cache error only | No effect (not a server signal) |
This classification fixes the previous bug where a successful /System/Info/Public ping reported "online" even while the user's authenticated data calls were failing — and vice versa.
Time-window debounce (offline) + instant recovery (online)
To stop the banner from flapping on a single dropped request, the transition to offline is debounced over a time window:
- On the first
Networkfailure, the monitor recordsfirst_failure_at. - It flips
is_server_reachable = falseonly onceNetworkfailures have persisted continuously forOFFLINE_CONFIRM_WINDOW(5s) with no intervening success. - Any success (or server-answered error) clears
first_failure_atand immediately marks reachable.
Recovery is therefore instant and asymmetric: one good response brings the app back online, but a brief blip never trips the banner.
Offline-only recovery probe
flowchart TB
Repo["OnlineRepository"] -->|"success / RepoError"| Monitor["ConnectivityMonitor"]
Monitor --> State{"is_server_reachable?"}
State -->|"Online"| NoProbe["No background polling<br/>(real traffic is the signal)"]
State -->|"Offline"| Probe["5s /System/Info/Public probe<br/>(recovery detector)"]
Probe -->|"reachable again"| Monitor
Monitor -->|"on change"| Emit["Emit connectivity:changed<br/>+ connectivity:reconnected"]
Emit --> Frontend["Frontend Store → banner"]
While online, there is no background polling — real requests keep the state fresh. While offline, the fast 5s probe runs so an idle app still detects the server returning even when no user traffic is flowing.
Features:
- Traffic-driven: Reachability follows the requests the user actually makes.
- Time-window debounce: Offline declared only after
OFFLINE_CONFIRM_WINDOW(5s) of sustained network failure; recovery is instant. - Offline-only probe: 5s
/System/Info/Publicprobe runs only while offline. - Event Emission: Emits
connectivity:changedandconnectivity:reconnectedevents. - Thread-Safe: Uses
Arc<RwLock<>>for shared state.
Tauri Commands:
| Command | Description |
|---|---|
connectivity_check_server |
Manual reachability check (also used by the frontend's advisory navigator.onLine hint) |
connectivity_set_server_url |
Update monitored server URL |
connectivity_get_status |
Get current connectivity status |
connectivity_start_monitoring |
Start the offline recovery probe |
connectivity_stop_monitoring |
Stop the probe |
connectivity_mark_reachable |
Mark reachable — driven by OnlineRepository on every server success |
connectivity_mark_unreachable |
Mark unreachable — driven by OnlineRepository on RepoError::Network (subject to debounce) |
Frontend Integration:
// The store is a pure reflection of backend events — it no longer decides
// reachability itself. navigator.onLine is advisory: it triggers an immediate
// recheck rather than forcing the offline state.
listen<{ isReachable: boolean }>("connectivity:changed", (event) => {
updateConnectivityState(event.payload.isReachable);
});
Network Resilience Architecture
The connectivity system provides resilience through multiple layers:
- HTTP Client Layer: Automatic retry with exponential backoff
- Connectivity Monitoring: Reachability derived from real repository traffic, with an offline-only recovery probe
- Frontend Integration: Offline mode detection and UI updates (a pure reflection of backend events)
- Sync Queue: Offline mutations queued for later (see 06-downloads-and-offline.md)
Design Principles:
- Single source of truth: Reachability follows the outcome of real requests, classified via
RepoError; the frontend store and the probe never compete to decide it. - Fail Fast: Don't retry 4xx errors (client errors, authentication).
- Fail Slow: Retry network and 5xx errors with increasing delays.
- Debounced offline, instant online: Declare offline only after a sustained failure window; recover on the first success.
- Probe only when needed: Background polling runs only while offline, as a recovery detector.
- Event-Driven: Frontend reacts to connectivity changes via events.