mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Improve loading speed of desktop app (#4865)
# Description of Changes Improve loading speed of desktop app by loading a default config until the backend has spawned.
This commit is contained in:
@@ -1,91 +1,24 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
|
||||
export type BackendStatus = 'starting' | 'healthy' | 'unhealthy' | 'stopped';
|
||||
|
||||
interface BackendHealthState {
|
||||
status: BackendStatus;
|
||||
message?: string;
|
||||
lastChecked?: number;
|
||||
isChecking: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { backendHealthMonitor } from '@app/services/backendHealthMonitor';
|
||||
import type { BackendHealthState } from '@app/types/backendHealth';
|
||||
|
||||
/**
|
||||
* Hook to monitor backend health status with retries
|
||||
* Hook to read the shared backend health monitor state.
|
||||
* All consumers subscribe to a single poller managed by backendHealthMonitor.
|
||||
*/
|
||||
export function useBackendHealth(pollingInterval = 5000) {
|
||||
const [health, setHealth] = useState<BackendHealthState>({
|
||||
status: tauriBackendService.isBackendRunning() ? 'healthy' : 'stopped',
|
||||
isChecking: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
setHealth((current) => ({
|
||||
...current,
|
||||
status: current.status === 'healthy' ? 'healthy' : 'starting',
|
||||
isChecking: true,
|
||||
error: 'Backend starting up...',
|
||||
lastChecked: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const isHealthy = await tauriBackendService.checkBackendHealth();
|
||||
|
||||
setHealth({
|
||||
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||
lastChecked: Date.now(),
|
||||
message: isHealthy ? 'Backend is healthy' : 'Backend is unavailable',
|
||||
isChecking: false,
|
||||
error: isHealthy ? null : 'Backend offline',
|
||||
});
|
||||
|
||||
return isHealthy;
|
||||
} catch (error) {
|
||||
console.error('[BackendHealth] Health check failed:', error);
|
||||
setHealth({
|
||||
status: 'unhealthy',
|
||||
lastChecked: Date.now(),
|
||||
message: 'Backend is unavailable',
|
||||
isChecking: false,
|
||||
error: 'Backend offline',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
export function useBackendHealth() {
|
||||
const [health, setHealth] = useState<BackendHealthState>(() => backendHealthMonitor.getSnapshot());
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
return backendHealthMonitor.subscribe(setHealth);
|
||||
}, []);
|
||||
|
||||
const initialize = async () => {
|
||||
setHealth((current) => ({
|
||||
...current,
|
||||
status: tauriBackendService.isBackendRunning() ? 'starting' : 'stopped',
|
||||
isChecking: true,
|
||||
error: 'Backend starting up...',
|
||||
}));
|
||||
|
||||
await checkHealth();
|
||||
if (!isMounted) return;
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!isMounted) return;
|
||||
void checkHealth();
|
||||
}, pollingInterval);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [checkHealth, pollingInterval]);
|
||||
const checkHealth = useCallback(async () => {
|
||||
return backendHealthMonitor.checkNow();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...health,
|
||||
isHealthy: health.status === 'healthy',
|
||||
checkHealth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { tauriBackendService } from '@app/services/tauriBackendService';
|
||||
import { isBackendNotReadyError } from '@app/constants/backendErrors';
|
||||
|
||||
interface EndpointConfig {
|
||||
backendUrl: string;
|
||||
}
|
||||
|
||||
const RETRY_DELAY_MS = 2500;
|
||||
|
||||
function getErrorMessage(err: unknown): string {
|
||||
if (isAxiosError(err)) {
|
||||
const data = err.response?.data as { message?: string } | undefined;
|
||||
if (typeof data?.message === 'string') {
|
||||
return data.message;
|
||||
}
|
||||
return err.message || 'Unknown error occurred';
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return 'Unknown error occurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop-specific endpoint checker that hits the backend directly via axios.
|
||||
*/
|
||||
@@ -14,38 +34,88 @@ export function useEndpointEnabled(endpoint: string): {
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
} {
|
||||
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
const [enabled, setEnabled] = useState<boolean | null>(() => (endpoint ? true : null));
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearRetryTimeout = useCallback(() => {
|
||||
if (retryTimeoutRef.current) {
|
||||
clearTimeout(retryTimeoutRef.current);
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
clearRetryTimeout();
|
||||
};
|
||||
}, [clearRetryTimeout]);
|
||||
|
||||
const fetchEndpointStatus = useCallback(async () => {
|
||||
clearRetryTimeout();
|
||||
|
||||
const fetchEndpointStatus = async () => {
|
||||
if (!endpoint) {
|
||||
if (!isMountedRef.current) return;
|
||||
setEnabled(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await apiClient.get<boolean>('/api/v1/config/endpoint-enabled', {
|
||||
params: { endpoint },
|
||||
suppressErrorToast: true,
|
||||
});
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
setEnabled(response.data);
|
||||
} catch (err: any) {
|
||||
const message = err?.response?.data?.message || err?.message || 'Unknown error occurred';
|
||||
setError(message);
|
||||
setEnabled(null);
|
||||
} catch (err: unknown) {
|
||||
const isBackendStarting = isBackendNotReadyError(err);
|
||||
const message = getErrorMessage(err);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(isBackendStarting ? t('backendHealth.starting', 'Backend starting up...') : message);
|
||||
setEnabled(true);
|
||||
|
||||
if (!retryTimeoutRef.current) {
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
retryTimeoutRef.current = null;
|
||||
fetchEndpointStatus();
|
||||
}, RETRY_DELAY_MS);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [endpoint, clearRetryTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEndpointStatus();
|
||||
}, [endpoint]);
|
||||
if (!endpoint) {
|
||||
setEnabled(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tauriBackendService.isBackendHealthy()) {
|
||||
fetchEndpointStatus();
|
||||
}
|
||||
|
||||
const unsubscribe = tauriBackendService.subscribeToStatus((status) => {
|
||||
if (status === 'healthy') {
|
||||
fetchEndpointStatus();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [endpoint, fetchEndpointStatus]);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
@@ -61,45 +131,101 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
} {
|
||||
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>(() => {
|
||||
if (!endpoints || endpoints.length === 0) return {};
|
||||
return endpoints.reduce((acc, endpointName) => {
|
||||
acc[endpointName] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearRetryTimeout = useCallback(() => {
|
||||
if (retryTimeoutRef.current) {
|
||||
clearTimeout(retryTimeoutRef.current);
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
clearRetryTimeout();
|
||||
};
|
||||
}, [clearRetryTimeout]);
|
||||
|
||||
const fetchAllEndpointStatuses = useCallback(async () => {
|
||||
clearRetryTimeout();
|
||||
|
||||
const fetchAllEndpointStatuses = async () => {
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
if (!isMountedRef.current) return;
|
||||
setEndpointStatus({});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const endpointsParam = endpoints.join(',');
|
||||
|
||||
const response = await apiClient.get<Record<string, boolean>>('/api/v1/config/endpoints-enabled', {
|
||||
params: { endpoints: endpointsParam },
|
||||
suppressErrorToast: true,
|
||||
});
|
||||
|
||||
if (!isMountedRef.current) return;
|
||||
setEndpointStatus(response.data);
|
||||
} catch (err: any) {
|
||||
const message = err?.response?.data?.message || err?.message || 'Unknown error occurred';
|
||||
setError(message);
|
||||
} catch (err: unknown) {
|
||||
const isBackendStarting = isBackendNotReadyError(err);
|
||||
const message = getErrorMessage(err);
|
||||
if (!isMountedRef.current) return;
|
||||
setError(isBackendStarting ? t('backendHealth.starting', 'Backend starting up...') : message);
|
||||
|
||||
const fallbackStatus = endpoints.reduce((acc, endpointName) => {
|
||||
acc[endpointName] = false;
|
||||
acc[endpointName] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(fallbackStatus);
|
||||
|
||||
if (!retryTimeoutRef.current) {
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
retryTimeoutRef.current = null;
|
||||
fetchAllEndpointStatuses();
|
||||
}, RETRY_DELAY_MS);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [endpoints, clearRetryTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllEndpointStatuses();
|
||||
}, [endpoints.join(',')]);
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
setEndpointStatus({});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tauriBackendService.isBackendHealthy()) {
|
||||
fetchAllEndpointStatuses();
|
||||
}
|
||||
|
||||
const unsubscribe = tauriBackendService.subscribeToStatus((status) => {
|
||||
if (status === 'healthy') {
|
||||
fetchAllEndpointStatuses();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [endpoints, fetchAllEndpointStatuses]);
|
||||
|
||||
return {
|
||||
endpointStatus,
|
||||
|
||||
Reference in New Issue
Block a user