import { useEffect, useState } from 'react'; import { kumaUrls, parseKumaStatus, ServerStatus, StatusPageConfig } from '../utils/kumaStatus'; /** Normal poll, and while a problem is showing (so recovery clears quickly). */ export const STATUS_POLL_MS = 5 * 60 * 1000; export const STATUS_POLL_PROBLEM_MS = 60 * 1000; const STATUS_FETCH_TIMEOUT_MS = 10 * 1000; const STATUS_BACKOFF_MAX_MS = 15 * 60 * 1000; const fetchJson = async (url: string, signal: AbortSignal): Promise => { const res = await fetch(url, { cache: 'no-store', credentials: 'omit', signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }; export const hasProblem = (s: ServerStatus | null): boolean => !!s && (s.homeserver === 'down' || s.calls === 'down' || s.login === 'down' || s.maintenance.length > 0); /** * [Gitea #124] Poll the homeserver's Kuma status page (none configured → no * requests, null). Only while the page is visible; every 5 min, every 60 s * while something is wrong, and at once when `urgent` turns true (this * client's connection dropped). Any failure → null ("no banner"), with * backoff: Kuma being unreachable must never look like Matrix being down. */ export const useServerStatus = ( page: StatusPageConfig | undefined, urgent = false, ): ServerStatus | null => { const [status, setStatus] = useState(null); const pageKey = page ? `${page.url}/${page.slug}` : ''; useEffect(() => { if (!page) { setStatus(null); return undefined; } const urls = kumaUrls(page); let timer: ReturnType | undefined; let controller: AbortController | undefined; let failures = 0; let stopped = false; let last: ServerStatus | null = null; const schedule = (ms: number) => { if (timer) clearTimeout(timer); timer = setTimeout(poll, ms); }; async function poll() { if (stopped) return; if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; controller?.abort(); controller = new AbortController(); const abort = controller; const timeout = setTimeout(() => abort.abort(), STATUS_FETCH_TIMEOUT_MS); try { const [pageJson, heartbeatJson] = await Promise.all([ fetchJson(urls.page, abort.signal), fetchJson(urls.heartbeat, abort.signal), ]); if (stopped) return; failures = 0; last = parseKumaStatus(pageJson, heartbeatJson, page!.groups); setStatus(last); // While this client's connection is lost, keep asking every minute: that // is how it learns Kuma has checked the server since the drop. schedule(hasProblem(last) || urgent ? STATUS_POLL_PROBLEM_MS : STATUS_POLL_MS); } catch { if (stopped) return; failures += 1; last = null; setStatus(null); schedule(Math.min(STATUS_POLL_PROBLEM_MS * 2 ** failures, STATUS_BACKOFF_MAX_MS)); } finally { clearTimeout(timeout); } } const onVisible = () => { if (document.visibilityState === 'visible') poll(); }; document.addEventListener('visibilitychange', onVisible); poll(); return () => { stopped = true; if (timer) clearTimeout(timer); controller?.abort(); document.removeEventListener('visibilitychange', onVisible); }; // pageKey identifies the page; the object itself is rebuilt from config. // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageKey, urgent]); return status; };