First working POC
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
// Connectivity state store for offline support
|
||||
//
|
||||
// Simplified wrapper over Rust connectivity monitor.
|
||||
// The Rust backend handles all polling, reachability checks, and adaptive intervals.
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user