// Connectivity state store for offline support // // Pure reflection of the Rust ConnectivityMonitor. Reachability is the single // source of truth in Rust, derived from real repository traffic (success/ // RepoError), with a time-window debounce before going offline and instant // recovery. This store only listens for `connectivity:changed` events and // mirrors status; it does not decide reachability itself. navigator.onLine is // advisory and triggers a recheck rather than forcing offline. // See docs/architecture/07-connectivity.md. // TRACES: UR-002 | DR-013 import { writable, derived } from "svelte/store"; import { browser } from "$app/environment"; import { listen } from "@tauri-apps/api/event"; import { commands } from "$lib/api/bindings"; export interface ConnectivityState { /** Browser's navigator.onLine status */ isOnline: boolean; /** Whether the Jellyfin server is actually reachable */ isServerReachable: boolean; /** Last time we checked server reachability */ lastChecked: Date | null; /** Error message from last connectivity check */ connectionError: string | null; /** Whether we're currently checking connectivity */ isChecking: boolean; } export interface ConnectivityEvents { /** Called when connectivity changes (online <-> offline) */ onConnectivityChange?: (isConnected: boolean) => void; /** Called when server becomes reachable after being unreachable */ onServerReconnected?: () => void; } function createConnectivityStore() { const initialState: ConnectivityState = { isOnline: browser ? navigator.onLine : true, // Start optimistic - assume server is reachable until proven otherwise // This prevents the app from appearing offline on startup isServerReachable: true, lastChecked: null, connectionError: null, isChecking: false, }; const { subscribe, set, update } = writable(initialState); let eventHandlers: ConnectivityEvents = {}; let isMonitoring = false; // Listen to connectivity change events from Rust if (browser) { listen<{ isReachable: boolean }>("connectivity:changed", (event) => { update((s) => ({ ...s, isServerReachable: event.payload.isReachable })); if (eventHandlers.onConnectivityChange) { eventHandlers.onConnectivityChange(event.payload.isReachable); } }); listen("connectivity:reconnected", () => { if (eventHandlers.onServerReconnected) { eventHandlers.onServerReconnected(); } }); // Listen to browser online/offline events. These are ADVISORY only — the // Rust ConnectivityMonitor (fed by real repository traffic) is the source of // truth for server reachability. navigator.onLine can be wrong (e.g. a // LAN-only server is still reachable while the browser reports "offline"), // so we use these events to trigger an immediate recheck rather than forcing // the offline state. See docs/architecture/07-connectivity.md. window.addEventListener("online", () => { update((s) => ({ ...s, isOnline: true })); // Device regained network — ask the backend to re-verify the server now. checkServerReachable().catch((err) => { console.debug("[ConnectivityStore] Recheck after 'online' failed:", err); }); }); window.addEventListener("offline", () => { // Reflect the browser's view of the device link, but let the backend // decide whether the server is actually reachable. update((s) => ({ ...s, isOnline: false })); checkServerReachable().catch((err) => { console.debug("[ConnectivityStore] Recheck after 'offline' failed:", err); }); }); } /** * Check if an error is a network error (vs auth/server error) * Kept for compatibility with existing code */ function isNetworkError(error: unknown): boolean { if (error instanceof TypeError) { return true; } if (error instanceof Error) { const msg = error.message.toLowerCase(); return ( msg.includes("network") || msg.includes("fetch") || msg.includes("failed to fetch") || msg.includes("networkerror") || msg.includes("connection") || msg.includes("timeout") || msg.includes("aborted") ); } return false; } /** * Check if the Jellyfin server is reachable (calls Rust) */ async function checkServerReachable(): Promise { try { const isReachable = await commands.connectivityCheckServer(); // Fetch updated status from Rust const status = await commands.connectivityGetStatus(); update((s) => ({ ...s, isServerReachable: status.isServerReachable, lastChecked: status.lastChecked ? new Date(status.lastChecked) : null, connectionError: status.connectionError, isChecking: status.isChecking, })); return isReachable; } catch (error) { console.error("[ConnectivityStore] Failed to check server:", error); return false; } } /** * Start monitoring connectivity (delegates to Rust) */ async function startMonitoring(url: string, handlers: ConnectivityEvents = {}): Promise { eventHandlers = handlers; isMonitoring = true; try { console.log("[ConnectivityStore] Starting monitoring for:", url); // Set the server URL await commands.connectivitySetServerUrl(url); // Start the Rust monitoring task (performs immediate check) await commands.connectivityStartMonitoring(); // Get the initial status immediately after starting const status = await commands.connectivityGetStatus(); update((s) => ({ ...s, isServerReachable: status.isServerReachable, lastChecked: status.lastChecked ? new Date(status.lastChecked) : null, connectionError: status.connectionError, isChecking: status.isChecking, })); console.log("[ConnectivityStore] Started monitoring. Initial status:", status.isServerReachable ? "ONLINE" : "OFFLINE"); } catch (error) { console.error("[ConnectivityStore] Failed to start monitoring:", error); update((s) => ({ ...s, isServerReachable: false, connectionError: "Failed to start monitoring", })); } } /** * Stop monitoring connectivity (delegates to Rust) */ async function stopMonitoring(): Promise { if (!isMonitoring) return; try { await commands.connectivityStopMonitoring(); isMonitoring = false; eventHandlers = {}; console.log("[ConnectivityStore] Stopped monitoring"); } catch (error) { console.error("[ConnectivityStore] Failed to stop monitoring:", error); } } /** * Update server URL (call when user changes servers) */ async function setServerUrl(url: string): Promise { try { await commands.connectivitySetServerUrl(url); } catch (error) { console.error("[ConnectivityStore] Failed to set server URL:", error); } } /** * Force a connectivity check */ async function forceCheck(): Promise { return checkServerReachable(); } // Reachability is now driven by the Rust backend from real repository // traffic (see docs/architecture/07-connectivity.md). The frontend no longer // mutates reachability itself, so the former markReachable/markUnreachable // helpers have been removed. The underlying Rust commands remain available // for deliberate signals if ever needed. return { subscribe, startMonitoring, stopMonitoring, setServerUrl, forceCheck, checkServerReachable, isNetworkError, }; } export const connectivity = createConnectivityStore(); // Derived stores for convenience export const isOnline = derived(connectivity, ($c) => $c.isOnline); export const isServerReachable = derived(connectivity, ($c) => $c.isServerReachable); export const isConnected = derived( connectivity, ($c) => $c.isOnline && $c.isServerReachable ); export const connectionError = derived(connectivity, ($c) => $c.connectionError);