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>
164 lines
5.5 KiB
Markdown
164 lines
5.5 KiB
Markdown
# Data Flow
|
|
|
|
## Repository Query Flow (Cache-First)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant UI as Svelte Component
|
|
participant Client as RepositoryClient (TS)
|
|
participant Rust as Tauri Command
|
|
participant Hybrid as HybridRepository
|
|
participant Cache as OfflineRepository (SQLite)
|
|
participant Server as OnlineRepository (HTTP)
|
|
participant Conn as ConnectivityMonitor
|
|
|
|
UI->>Client: getItems(parentId)
|
|
Client->>Rust: invoke("repository_get_items", {handle, parentId})
|
|
Rust->>Hybrid: get_items()
|
|
|
|
par Parallel Racing
|
|
Hybrid->>Cache: get_items() with 100ms timeout
|
|
Hybrid->>Server: get_items() (no timeout)
|
|
end
|
|
|
|
Note over Server,Conn: Every server request reports its outcome
|
|
alt Server succeeds (or answers with 4xx/5xx)
|
|
Server->>Conn: mark_reachable() (server is up)
|
|
else Network failure / timeout
|
|
Server->>Conn: mark_unreachable() (debounced)
|
|
end
|
|
|
|
alt Cache returns with content
|
|
Cache-->>Hybrid: Result with items
|
|
Hybrid-->>Rust: Return cache result
|
|
else Cache timeout or empty
|
|
Server-->>Hybrid: Fresh result
|
|
Hybrid-->>Rust: Return server result
|
|
end
|
|
|
|
Rust-->>Client: SearchResult
|
|
Client-->>UI: items[]
|
|
Note over UI: Reactive update
|
|
```
|
|
|
|
**Key Points:**
|
|
- Cache queries have 100ms timeout for responsiveness
|
|
- Server queries always run for fresh data
|
|
- Cache wins if it has meaningful content
|
|
- Automatic fallback to server if cache is empty/stale
|
|
- Background cache updates (planned)
|
|
- **Connectivity side-effect**: each server request feeds the `ConnectivityMonitor`, which is the source of truth for the offline/online banner (see [07-connectivity.md](07-connectivity.md)). A server-answered error (401/404/5xx) still counts as *reachable* — only network failures, sustained past a debounce window, flip the app to offline.
|
|
|
|
## Playback Initiation Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant AudioPlayer
|
|
participant Tauri as Tauri IPC
|
|
participant Command as player_play_item()
|
|
participant Controller as PlayerController
|
|
participant Backend as PlayerBackend
|
|
participant Store as Frontend Store
|
|
|
|
User->>AudioPlayer: clicks play
|
|
AudioPlayer->>Tauri: invoke("player_play_item", {item})
|
|
Tauri->>Command: player_play_item()
|
|
Command->>Command: Convert PlayItemRequest -> MediaItem
|
|
Command->>Controller: play_item(item)
|
|
Controller->>Backend: load(item)
|
|
Note over Backend: State -> Loading
|
|
Controller->>Backend: play()
|
|
Note over Backend: State -> Playing
|
|
Controller-->>Command: Ok(())
|
|
Command-->>Tauri: PlayerStatus {state, position, duration, volume}
|
|
Tauri-->>AudioPlayer: status
|
|
AudioPlayer->>Store: player.setPlaying(media, position, duration)
|
|
Note over Store: UI updates reactively
|
|
```
|
|
|
|
## Playback Mode Transfer Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant UI as Cast Button
|
|
participant Store as playbackMode store
|
|
participant Rust as Tauri Command
|
|
participant Manager as PlaybackModeManager
|
|
participant Player as PlayerController
|
|
participant Jellyfin as Jellyfin API
|
|
|
|
UI->>Store: transferToRemote(sessionId)
|
|
Store->>Rust: invoke("playback_mode_transfer_to_remote", {sessionId})
|
|
Rust->>Manager: transfer_to_remote()
|
|
|
|
Manager->>Player: Get current queue
|
|
Player-->>Manager: Vec<MediaItem>
|
|
Manager->>Manager: Extract Jellyfin IDs
|
|
|
|
Manager->>Jellyfin: POST /Sessions/{id}/Playing<br/>{itemIds, startIndex}
|
|
Jellyfin-->>Manager: 200 OK
|
|
|
|
Manager->>Jellyfin: POST /Sessions/{id}/Playing/Seek<br/>{positionTicks}
|
|
Jellyfin-->>Manager: 200 OK
|
|
|
|
Manager->>Player: stop()
|
|
Manager->>Manager: mode = Remote {sessionId}
|
|
|
|
Manager-->>Rust: Ok(())
|
|
Rust-->>Store: PlaybackMode
|
|
Store->>UI: Update cast icon
|
|
```
|
|
|
|
## Queue Navigation Flow
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
User["User clicks Next"] --> Invoke["invoke('player_next')"]
|
|
Invoke --> ControllerNext["controller.next()"]
|
|
ControllerNext --> QueueNext["queue.next()<br/>- Check repeat mode<br/>- Check shuffle<br/>- Update history"]
|
|
|
|
QueueNext --> None["None<br/>(at end)"]
|
|
QueueNext --> Some["Some(next)"]
|
|
QueueNext --> Same["Same<br/>(repeat one)"]
|
|
|
|
Some --> PlayItem["play_item(next)<br/>Returns new status"]
|
|
```
|
|
|
|
## Volume Control Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Slider as Volume Slider
|
|
participant Handler as handleVolumeChange()
|
|
participant Tauri as Tauri IPC
|
|
participant Command as player_set_volume
|
|
participant Controller as PlayerController
|
|
participant Backend as MpvBackend/NullBackend
|
|
participant Events as playerEvents.ts
|
|
participant Store as Player Store
|
|
participant UI
|
|
|
|
User->>Slider: adjusts (0-100)
|
|
Slider->>Handler: oninput event
|
|
Handler->>Handler: Convert 0-100 -> 0.0-1.0
|
|
Handler->>Tauri: invoke("player_set_volume", {volume})
|
|
Tauri->>Command: player_set_volume
|
|
Command->>Controller: set_volume(volume)
|
|
Controller->>Backend: set_volume(volume)
|
|
Backend->>Backend: Clamp to 0.0-1.0
|
|
Note over Backend: MpvBackend: Send to MPV loop
|
|
Backend-->>Tauri: emit "player-event"
|
|
Tauri-->>Events: VolumeChanged event
|
|
Events->>Store: player.setVolume(volume)
|
|
Store-->>UI: Reactive update
|
|
Note over UI: Both AudioPlayer and<br/>MiniPlayer stay in sync
|
|
```
|
|
|
|
**Key Implementation Details:**
|
|
- Volume is stored in the backend (NullBackend/MpvBackend)
|
|
- `PlayerController.volume()` delegates to backend
|
|
- `get_player_status()` returns `controller.volume()` (not hardcoded)
|
|
- Frontend uses normalized 0.0-1.0 scale, UI shows 0-100
|