Some checks failed
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 3m32s
Traceability Validation / Check Requirement Traces (push) Successful in 20s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m27s
Build & Release / Run Tests (push) Successful in 3m36s
Build & Release / Build Linux (push) Successful in 15m43s
Build & Release / Build Android (push) Successful in 18m40s
Build & Release / Create Release (push) Failing after 22s
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>
128 lines
6.4 KiB
Markdown
128 lines
6.4 KiB
Markdown
# 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:
|
|
|
|
```rust
|
|
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:**
|
|
```rust
|
|
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** `Network` failure, the monitor records `first_failure_at`.
|
|
- It flips `is_server_reachable = false` only once `Network` failures have persisted continuously for `OFFLINE_CONFIRM_WINDOW` (5s) with no intervening success.
|
|
- **Any** success (or server-answered error) clears `first_failure_at` and 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
|
|
|
|
```mermaid
|
|
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/Public` probe runs only while offline.
|
|
- **Event Emission**: Emits `connectivity:changed` and `connectivity:reconnected` events.
|
|
- **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:**
|
|
```typescript
|
|
// 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:
|
|
|
|
1. **HTTP Client Layer**: Automatic retry with exponential backoff
|
|
2. **Connectivity Monitoring**: Reachability derived from real repository traffic, with an offline-only recovery probe
|
|
3. **Frontend Integration**: Offline mode detection and UI updates (a pure reflection of backend events)
|
|
4. **Sync Queue**: Offline mutations queued for later (see [06-downloads-and-offline.md](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.
|