feat: implement sync retry mechanism in ClientRoot with timeout handling

This commit is contained in:
2026-04-07 16:44:21 +10:00
parent f64ce6168d
commit 0659f3c1b0

View File

@@ -15,7 +15,7 @@ import {
} from 'folds'; } from 'folds';
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk'; import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react'; import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { import {
clearCacheAndReload, clearCacheAndReload,
clearLoginData, clearLoginData,
@@ -155,9 +155,13 @@ const useBackgroundSync = (mx?: MatrixClient) => {
type ClientRootProps = { type ClientRootProps = {
children: ReactNode; children: ReactNode;
}; };
const MAX_SYNC_RETRIES = 3;
export function ClientRoot({ children }: ClientRootProps) { export function ClientRoot({ children }: ClientRootProps) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [syncError, setSyncError] = useState<Error | null>(null); const [syncError, setSyncError] = useState<Error | null>(null);
const syncRetryCount = useRef(0);
const mxRef = useRef<MatrixClient | undefined>(undefined);
const { baseUrl } = getCurrentSession() ?? {}; const { baseUrl } = getCurrentSession() ?? {};
// Enable view transitions for smooth navigation // Enable view transitions for smooth navigation
@@ -173,6 +177,11 @@ export function ClientRoot({ children }: ClientRootProps) {
}, []) }, [])
); );
const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined; const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
useEffect(() => {
mxRef.current = mx;
}, [mx]);
const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>( const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
useCallback((m) => startClient(m), []) useCallback((m) => startClient(m), [])
); );
@@ -193,14 +202,14 @@ export function ClientRoot({ children }: ClientRootProps) {
} }
}, [mx, startMatrix]); }, [mx, startMatrix]);
// Timeout if sync takes too long // Timeout if sync takes too long - nudge the client rather than immediately erroring
useEffect(() => { useEffect(() => {
if (!mx || !loading) return; if (!mx || !loading) return;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
console.error('[ClientRoot] Sync timeout - client failed to reach PREPARED state'); console.warn('[ClientRoot] Sync taking too long, nudging client...');
setSyncError(new Error('Sync timeout - failed to connect to homeserver')); mx.retryImmediately();
}, 30000); // 30 second timeout }, 30000);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [mx, loading]); }, [mx, loading]);
@@ -208,13 +217,24 @@ export function ClientRoot({ children }: ClientRootProps) {
useSyncState( useSyncState(
mx, mx,
useCallback((state, prevState, data) => { useCallback((state, prevState, data) => {
const client = mxRef.current;
if (state === 'PREPARED') { if (state === 'PREPARED') {
setLoading(false); setLoading(false);
setSyncError(null); setSyncError(null);
syncRetryCount.current = 0;
} else if (state === 'ERROR') { } else if (state === 'ERROR') {
const error = data instanceof Error ? data : new Error('Sync failed'); if (syncRetryCount.current < MAX_SYNC_RETRIES) {
console.error('[ClientRoot] Sync error:', error); syncRetryCount.current += 1;
setSyncError(error); const attempt = syncRetryCount.current;
console.warn(`[ClientRoot] Sync error, auto-retrying (${attempt}/${MAX_SYNC_RETRIES})...`);
setTimeout(() => {
if (client) startClient(client).catch(() => {});
}, 2000 * attempt);
} else {
const error = data instanceof Error ? data : new Error('Sync failed');
console.error('[ClientRoot] Sync error after max retries:', error);
setSyncError(error);
}
} }
}, []) }, [])
); );