jellytau/src/lib/stores/connectivity.ts
Duncan Tourolle e3797f32ca
Some checks failed
Traceability Validation / Check Requirement Traces (push) Failing after 1m18s
🏗️ Build and Test JellyTau / Build APK and Run Tests (push) Has been cancelled
many changes
2026-02-14 00:09:47 +01:00

270 lines
7.8 KiB
TypeScript

// Connectivity state store for offline support
//
// Simplified wrapper over Rust connectivity monitor.
// The Rust backend handles all polling, reachability checks, and adaptive intervals.
// TRACES: UR-002 | DR-013
import { writable, derived } from "svelte/store";
import { browser } from "$app/environment";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
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;
}
interface RustConnectivityStatus {
isServerReachable: boolean;
lastChecked: string | null;
connectionError: string | null;
isChecking: boolean;
}
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<ConnectivityState>(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 and update state
window.addEventListener("online", () => {
update((s) => ({ ...s, isOnline: true }));
});
window.addEventListener("offline", () => {
update((s) => ({
...s,
isOnline: false,
isServerReachable: false,
connectionError: "Device is offline",
}));
if (eventHandlers.onConnectivityChange) {
eventHandlers.onConnectivityChange(false);
}
});
}
/**
* 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<boolean> {
try {
const isReachable = await invoke<boolean>("connectivity_check_server");
// Fetch updated status from Rust
const status = await invoke<RustConnectivityStatus>("connectivity_get_status");
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<void> {
eventHandlers = handlers;
isMonitoring = true;
try {
console.log("[ConnectivityStore] Starting monitoring for:", url);
// Set the server URL
await invoke("connectivity_set_server_url", { url });
// Start the Rust monitoring task (performs immediate check)
await invoke("connectivity_start_monitoring");
// Get the initial status immediately after starting
const status = await invoke<RustConnectivityStatus>("connectivity_get_status");
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<void> {
if (!isMonitoring) return;
try {
await invoke("connectivity_stop_monitoring");
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<void> {
try {
await invoke("connectivity_set_server_url", { url });
} catch (error) {
console.error("[ConnectivityStore] Failed to set server URL:", error);
}
}
/**
* Force a connectivity check
*/
async function forceCheck(): Promise<boolean> {
return checkServerReachable();
}
/**
* Mark server as reachable (e.g., after successful API call)
*/
async function markReachable(): Promise<void> {
try {
await invoke("connectivity_mark_reachable");
// Update local state
update((s) => ({
...s,
isServerReachable: true,
lastChecked: new Date(),
connectionError: null,
}));
} catch (error) {
console.error("[ConnectivityStore] Failed to mark reachable:", error);
}
}
/**
* Mark server as unreachable (e.g., after failed API call)
*/
async function markUnreachable(error?: string): Promise<void> {
try {
await invoke("connectivity_mark_unreachable", { error: error ?? null });
// Update local state
update((s) => ({
...s,
isServerReachable: false,
lastChecked: new Date(),
connectionError: error || "Server unreachable",
}));
} catch (err) {
console.error("[ConnectivityStore] Failed to mark unreachable:", err);
}
}
return {
subscribe,
startMonitoring,
stopMonitoring,
setServerUrl,
forceCheck,
checkServerReachable,
markReachable,
markUnreachable,
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);