import { toast } from 'sonner'; // v1.8.1 thresholds. First disconnect is silent — mobile Authelia idle timeouts // and tab suspensions trip reconnects constantly and the old red "websocket // error" toast made the app feel broken. Only escalate once the failure is // sustained. const TOAST_AFTER_FAILS = 3; const TOAST_AFTER_MS = 15_000; const PERSISTENT_AFTER_MS = 60_000; export interface WsReconnectToast { onFailure(): void; onConnected(): void; dispose(): void; } interface Options { label: string; // shown in the toast (e.g. "Live updates") onRetryNow: () => void; // user clicked the "Retry now" action } // Per-connection toast wrapper. Caller drives it from the WS lifecycle: // onFailure — after each failed connection attempt // onConnected — after a successful onopen // dispose — on hook unmount // The wrapper itself runs no timers and does not change the caller's reconnect // cadence; it only decides when to show / dismiss the toast. export function createWsReconnectToast(opts: Options): WsReconnectToast { let firstFailureAt: number | null = null; let failureCount = 0; let reconnectingId: string | number | null = null; let persistentId: string | number | null = null; function dismissReconnecting(): void { if (reconnectingId !== null) { toast.dismiss(reconnectingId); reconnectingId = null; } } function dismissPersistent(): void { if (persistentId !== null) { toast.dismiss(persistentId); persistentId = null; } } return { onFailure() { if (firstFailureAt === null) firstFailureAt = Date.now(); failureCount += 1; const elapsed = Date.now() - firstFailureAt; // Escalate to red error + Retry button after PERSISTENT_AFTER_MS. Replaces // the gray toast if it's still showing. if (persistentId === null && elapsed >= PERSISTENT_AFTER_MS) { dismissReconnecting(); persistentId = toast.error(`${opts.label}: connection lost`, { duration: Infinity, action: { label: 'Retry now', onClick: () => { dismissReconnecting(); dismissPersistent(); opts.onRetryNow(); }, }, }); return; } // Gray "reconnecting…" toast once we've crossed either threshold. if ( reconnectingId === null && persistentId === null && (failureCount >= TOAST_AFTER_FAILS || elapsed >= TOAST_AFTER_MS) ) { reconnectingId = toast.warning(`${opts.label}: reconnecting…`, { duration: Infinity, }); } }, onConnected() { firstFailureAt = null; failureCount = 0; dismissReconnecting(); dismissPersistent(); }, dispose() { firstFailureAt = null; failureCount = 0; dismissReconnecting(); dismissPersistent(); }, }; }