Files
jellytau/src/lib/stores/connectivity.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

253 lines
8.7 KiB
TypeScript

// 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, UR-043, UR-052 | DR-013, DR-055, DR-079
import { writable, derived } from "svelte/store";
import { browser } from "$app/environment";
import { listen } from "@tauri-apps/api/event";
import { commands } from "$lib/api/bindings";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("ConnectivityStore");
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<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. 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) => {
log.debug("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) => {
log.debug("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<boolean> {
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) {
log.error("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 {
log.debug("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,
}));
log.debug(
"Started monitoring. Initial status:",
status.isServerReachable ? "ONLINE" : "OFFLINE",
);
} catch (error) {
log.error("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 commands.connectivityStopMonitoring();
isMonitoring = false;
eventHandlers = {};
log.debug("Stopped monitoring");
} catch (error) {
log.error("Failed to stop monitoring:", error);
}
}
/**
* Update server URL (call when user changes servers)
*/
async function setServerUrl(url: string): Promise<void> {
try {
await commands.connectivitySetServerUrl(url);
} catch (error) {
log.error("Failed to set server URL:", error);
}
}
/**
* Force a connectivity check
*/
async function forceCheck(): Promise<boolean> {
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);
// "Connected" follows backend reachability ALONE — not navigator.onLine.
// The Rust ConnectivityMonitor (fed by real repository traffic) is the source
// of truth (DR-055); navigator.onLine is advisory and can be wrong (server
// unreachable on a live device link — server down, wrong LAN, dropped VPN —
// still reports online). Folding it in kept `isConnected` true in exactly those
// cases and prevented the offline "downloaded only" gate from ever closing.
// navigator.onLine stays a *trigger* for a recheck (the online/offline
// listeners call checkServerReachable), never a *term* in this decision.
// The startup default (isServerReachable: true) is intentionally optimistic —
// a brief full-catalog flash before the first probe beats flipping the app to
// "offline" on launch. See docs/architecture/07-connectivity.md.
// TRACES: UR-052 | DR-079
export const isConnected = derived(connectivity, ($c) => $c.isServerReachable);
export const connectionError = derived(connectivity, ($c) => $c.connectionError);