# 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. ## Search Flow (Locally Indexed) **TRACES**: UR-065 | DR-108 … DR-111, IR-030 Search does not depend on a per-keystroke round trip to Jellyfin. The instant leg reads the **local SQLite catalog**, which is already synced and already FTS5-indexed, so results appear as fast as SQLite can answer — online or offline. The server query stays, demoted to a background reconciliation that merges in late results. ```mermaid sequenceDiagram participant UI as Search UI participant Rust as repository_search participant Cache as Local catalog (FTS5) participant Server as Jellyfin participant Indexer as spawn_catalog_indexer UI->>Rust: search(query, scope) Rust->>Cache: FTS5 query, scope expanded by SearchScope::item_types() Cache-->>UI: instant results Rust->>Server: reconciliation query (background) Server-->>UI: search-event with late/merged results Note over Indexer,Cache: Independent of any query:
scheduled crawl keeps the index fresh,
prunes items deleted on the server ``` **Key points:** - The **scope is opaque on the wire**. The frontend sends a `SearchScope` variant; Rust expands it to item types ([01-rust-backend.md](01-rust-backend.md#search-scope-and-the-taxonomy-boundary)). - **Index freshness is a Rust policy**, not a frontend startup call — a scheduled background pass, not "whatever was synced when the app last launched" (DR-109). See [Background workers](01-rust-backend.md#background-workers). - **Index hygiene matters as much as freshness**: the catalog save path uses `INSERT OR REPLACE` and the crawl prunes rows for content deleted on the server, or search keeps returning items that no longer exist (DR-110). - The index covers **exactly the types the result groups render** (DR-111) — including Artists, which the crawl must reach or the Artists group is silently always empty. **Deliberately not done, with reasons:** - **Incremental indexing** (Jellyfin's `MinDateLastSaved`). A *full* crawl is what makes the deletion sweep sound — it yields the authoritative id set per library, and an incremental pass cannot detect deletions. Worth revisiting if full crawls prove slow on large libraries; measure first. - **Removing the server leg.** The reconciliation query stays. > ⚠️ Two dead search implementations still exist: `storage_search_items` > (`commands/storage/mod.rs`) and `offline_search` (`commands/offline.rs`). Both > are registered in `lib.rs` and exported to `bindings.ts`; neither is called > from the frontend. Deleting them is correct and unclaimed. ## 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 Manager->>Manager: Extract Jellyfin IDs Manager->>Jellyfin: POST /Sessions/{id}/Playing
{itemIds, startIndex} Jellyfin-->>Manager: 200 OK Manager->>Jellyfin: POST /Sessions/{id}/Playing/Seek
{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()
- Check repeat mode
- Check shuffle
- Update history"] QueueNext --> None["None
(at end)"] QueueNext --> Some["Some(next)"] QueueNext --> Same["Same
(repeat one)"] Some --> PlayItem["play_item(next)
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
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