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