Some checks failed
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 3m32s
Traceability Validation / Check Requirement Traces (push) Successful in 20s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m27s
Build & Release / Run Tests (push) Successful in 3m36s
Build & Release / Build Linux (push) Successful in 15m43s
Build & Release / Build Android (push) Successful in 18m40s
Build & Release / Create Release (push) Failing after 22s
The offline/online switch was janky because two independent systems decided "online" and never communicated: - ConnectivityMonitor owned is_server_reachable (drove the UI banner) but learned reachability only from a standalone /System/Info/Public ping loop and from auth/login calls. - HybridRepository served all real data by racing cache-vs-server but never read or wrote reachability. So the banner reflected a side-channel poller, not the system the user actually experienced: a successful ping could read "online" while authenticated data calls 401'd or timed out, and three different timeout regimes (5s ping / 30s data / 100ms cache race) flapped against each other. Unify into a single source of truth: - Extract a cheap, cloneable ConnectivityReporter that owns all reachability transitions and event emission. - OnlineRepository reports the outcome of every server request to the reporter, classified via RepoError: Ok/Authentication/NotFound/Server => reachable (the server answered), Network => offline candidate, Database/Offline => ignored (not a server signal). - Time-window debounce (OFFLINE_CONFIRM_WINDOW = 5s): flip offline only after sustained network failure; recover instantly on the first success. - Demote the ping loop to an offline-only recovery probe (no online polling; real traffic is the signal when online). - Frontend: navigator.onLine is now advisory (triggers a recheck instead of forcing offline); removed the dead markReachable/markUnreachable store methods. Docs updated (README, 07-connectivity, 03-data-flow, 02-svelte-frontend) to describe the new model and fix pre-existing drift (HTTP client is 30s timeout + 5s ping, not the documented 10s/base_url). Tests: 12 connectivity tests (debounce, instant recovery, RepoError classification through report_outcome). Full suite: 398 Rust + 384 frontend passing, svelte-check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
239 lines
7.9 KiB
TypeScript
239 lines
7.9 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 | 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<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) => {
|
|
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<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) {
|
|
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 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<void> {
|
|
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<void> {
|
|
try {
|
|
await commands.connectivitySetServerUrl(url);
|
|
} catch (error) {
|
|
console.error("[ConnectivityStore] 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);
|
|
export const isConnected = derived(
|
|
connectivity,
|
|
($c) => $c.isOnline && $c.isServerReachable
|
|
);
|
|
export const connectionError = derived(connectivity, ($c) => $c.connectionError);
|