import { useEffect, useState } from 'react'; import { atom } from 'jotai'; import { ClientEvent, MatrixClient, SyncState } from 'matrix-js-sdk'; /** * [Gitea #317] A hung homeserver (accepts connections, never answers) went * unnoticed for up to 110 s: the SDK only gives up on a /sync long-poll after * pollTimeout (30 s) + an 80 s buffer, and until then the sync state stays * SYNCING, so no strip showed and Kuma wasn't asked. This watchdog notices a * /sync that's overdue and confirms with a short probe of /versions. */ export const STALL_AFTER_MS = 45_000; // a healthy long-poll returns within ~30 s const CHECK_EVERY_MS = 10_000; const PROBE_TIMEOUT_MS = 5_000; export const homeserverStalledAtom = atom(false); /** Pure: should we probe, given the last sync response time? */ export const syncOverdue = (lastSyncAt: number, now: number, state: SyncState | null): boolean => state === SyncState.Syncing && now - lastSyncAt > STALL_AFTER_MS; export function useHomeserverStall(mx: MatrixClient): boolean { const [stalled, setStalled] = useState(false); useEffect(() => { let lastSyncAt = Date.now(); let probing = false; const onSync = () => { lastSyncAt = Date.now(); setStalled(false); }; const check = async () => { if (probing || !syncOverdue(lastSyncAt, Date.now(), mx.getSyncState())) return; probing = true; let answered = false; try { const res = await fetch(`${mx.getHomeserverUrl()}/_matrix/client/versions`, { cache: 'no-store', signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), }); answered = res.ok; } catch { answered = false; } probing = false; // Still overdue and the server didn't answer a trivial request: stalled. if (!answered && syncOverdue(lastSyncAt, Date.now(), mx.getSyncState())) setStalled(true); if (answered) setStalled(false); }; mx.on(ClientEvent.Sync, onSync); const id = setInterval(check, CHECK_EVERY_MS); return () => { mx.removeListener(ClientEvent.Sync, onSync); clearInterval(id); }; }, [mx]); return stalled; }