From 0659f3c1b037c71555714d2795bcbf3593c67323 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Tue, 7 Apr 2026 16:44:21 +1000 Subject: [PATCH] feat: implement sync retry mechanism in ClientRoot with timeout handling --- src/app/pages/client/ClientRoot.tsx | 36 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 9413f10..3a3ff23 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -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(null); + const syncRetryCount = useRef(0); + const mxRef = useRef(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( 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); + } } }, []) );