Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8fd0a1112 |
@@ -6,13 +6,12 @@ import {
|
||||
useCallJoined,
|
||||
useCallEmbedPlacementSync,
|
||||
useCallLoadError,
|
||||
useCallStart,
|
||||
} from '../../hooks/useCallEmbed';
|
||||
import { callEmbedAtom } from '../../state/callEmbed';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { PrescreenControls } from './PrescreenControls';
|
||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
@@ -161,16 +160,6 @@ function CallLoadErrorMessage() {
|
||||
// prescreen, where they can choose to join again.
|
||||
const dismiss = () => setCallEmbed(undefined);
|
||||
|
||||
// [Gitea #301] Try again: start a fresh embed for the same room with the
|
||||
// same mic/camera/sound choices (starting one disposes the hung one).
|
||||
const callEmbed = useCallEmbed();
|
||||
const startCall = useCallStart(useIsDirectRoom());
|
||||
const retry = () => {
|
||||
if (!callEmbed) return;
|
||||
const { room, control } = callEmbed;
|
||||
startCall(room, { microphone: control.microphone, video: control.video, sound: control.sound });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={css.CallViewContent}
|
||||
@@ -184,9 +173,6 @@ function CallLoadErrorMessage() {
|
||||
The call failed to load. Check your connection and try again.
|
||||
</Text>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Button variant="Primary" size="300" radii="400" onClick={retry} disabled={!callEmbed}>
|
||||
<Text size="B400">Try again</Text>
|
||||
</Button>
|
||||
<Button variant="Secondary" fill="Soft" size="300" radii="400" onClick={dismiss}>
|
||||
<Text size="B400">Back</Text>
|
||||
</Button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useServerStatus } from '../../hooks/useServerStatus';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { serverStatusAtom } from '../../state/serverStatus';
|
||||
import { pickBanner, resolveStatusPage, StatusBanner } from '../../utils/kumaStatus';
|
||||
import { homeserverStalledAtom, useHomeserverStall } from '../../hooks/useHomeserverStall';
|
||||
|
||||
const DISMISS_KEY = 'lotus-status-banner-dismissed';
|
||||
const DISMISS_MAX = 50;
|
||||
@@ -163,7 +164,12 @@ export function ServerStatusFeature({ mx }: { mx: MatrixClient }) {
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
const syncLost = lostAt !== null;
|
||||
// [Gitea #317] A hung server keeps sync "SYNCING" for up to 110 s; the stall
|
||||
// watchdog flags it sooner, and Kuma is asked at once.
|
||||
const stalled = useHomeserverStall(mx);
|
||||
const setStalled = useSetAtom(homeserverStalledAtom);
|
||||
useEffect(() => setStalled(stalled), [setStalled, stalled]);
|
||||
const syncLost = lostAt !== null || stalled;
|
||||
const status = useServerStatus(page, syncLost);
|
||||
const setAtom = useSetAtom(serverStatusAtom);
|
||||
useEffect(() => setAtom({ status, syncLost, lostAt }), [setAtom, status, syncLost, lostAt]);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { SyncState } from 'matrix-js-sdk';
|
||||
import { STALL_AFTER_MS, syncOverdue } from './useHomeserverStall';
|
||||
|
||||
test('syncOverdue: only while SYNCING and past the stall threshold (#317)', () => {
|
||||
const now = 1_000_000;
|
||||
assert.equal(syncOverdue(now - STALL_AFTER_MS - 1, now, SyncState.Syncing), true);
|
||||
assert.equal(syncOverdue(now - 20_000, now, SyncState.Syncing), false, 'a normal long-poll');
|
||||
assert.equal(
|
||||
syncOverdue(now - 120_000, now, SyncState.Reconnecting),
|
||||
false,
|
||||
'SyncStatus already shows that',
|
||||
);
|
||||
assert.equal(syncOverdue(now - 120_000, now, null), false);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useSyncState } from '../../hooks/useSyncState';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { serverStatusAtom } from '../../state/serverStatus';
|
||||
import { serverConfirmedUpSince } from '../../utils/kumaStatus';
|
||||
import { homeserverStalledAtom } from '../../hooks/useHomeserverStall';
|
||||
|
||||
type StateData = {
|
||||
current: SyncState | null;
|
||||
@@ -17,6 +18,7 @@ type SyncStatusProps = {
|
||||
};
|
||||
export function SyncStatus({ mx }: SyncStatusProps) {
|
||||
const serverStatus = useAtomValue(serverStatusAtom);
|
||||
const stalled = useAtomValue(homeserverStalledAtom);
|
||||
const [stateData, setStateData] = useState<StateData>({
|
||||
current: null,
|
||||
previous: undefined,
|
||||
@@ -34,6 +36,35 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
||||
}, []),
|
||||
);
|
||||
|
||||
// [Gitea #124] When Kuma confirms the homeserver is down, the status banner
|
||||
// says so ("Lotus Chat's server is down…") instead of "Connection Lost!".
|
||||
const serverDown = serverStatus.status?.homeserver === 'down';
|
||||
// Only blame the user's connection when Kuma has checked the server since
|
||||
// the drop and it was fine (a stale "up" from before proves nothing).
|
||||
const serverUp = serverConfirmedUpSince(serverStatus.status, serverStatus.lostAt);
|
||||
|
||||
// [Gitea #317] Sync still says SYNCING, but the server has stopped answering.
|
||||
if (stalled && stateData.current === SyncState.Syncing && !serverDown) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Warning' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Text size="L400">
|
||||
Lotus Chat's server isn't responding. Messages will send when it's back.
|
||||
Still trying…
|
||||
</Text>
|
||||
</Box>
|
||||
<Line variant="Warning" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(stateData.current === SyncState.Prepared ||
|
||||
stateData.current === SyncState.Syncing ||
|
||||
@@ -57,13 +88,6 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// [Gitea #124] When Kuma confirms the homeserver is down, the status banner
|
||||
// says so ("Lotus Chat's server is down…") instead of "Connection Lost!".
|
||||
const serverDown = serverStatus.status?.homeserver === 'down';
|
||||
// Only blame the user's connection when Kuma has checked the server since
|
||||
// the drop and it was fine (a stale "up" from before proves nothing).
|
||||
const serverUp = serverConfirmedUpSince(serverStatus.status, serverStatus.lostAt);
|
||||
|
||||
if (stateData.current === SyncState.Reconnecting && !serverDown) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
|
||||
Reference in New Issue
Block a user