mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +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:
@@ -8,7 +8,7 @@ import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext";
|
||||
import { HotkeyProvider } from "@app/contexts/HotkeyContext";
|
||||
import { SidebarProvider } from "@app/contexts/SidebarContext";
|
||||
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
|
||||
import { AppConfigProvider, AppConfigRetryOptions } from "@app/contexts/AppConfigContext";
|
||||
import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions } from "@app/contexts/AppConfigContext";
|
||||
import { RightRailProvider } from "@app/contexts/RightRailContext";
|
||||
import { ViewerProvider } from "@app/contexts/ViewerContext";
|
||||
import { SignatureProvider } from "@app/contexts/SignatureContext";
|
||||
@@ -31,22 +31,29 @@ function AppInitializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid requirement to have props which are required in app providers anyway
|
||||
type AppConfigProviderOverrides = Omit<AppConfigProviderProps, 'children' | 'retryOptions'>;
|
||||
|
||||
export interface AppProvidersProps {
|
||||
children: ReactNode;
|
||||
appConfigRetryOptions?: AppConfigRetryOptions;
|
||||
appConfigProviderProps?: Partial<AppConfigProviderOverrides>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core application providers
|
||||
* Contains all providers needed for the core
|
||||
*/
|
||||
export function AppProviders({ children, appConfigRetryOptions }: AppProvidersProps) {
|
||||
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
|
||||
return (
|
||||
<PreferencesProvider>
|
||||
<RainbowThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<OnboardingProvider>
|
||||
<AppConfigProvider retryOptions={appConfigRetryOptions}>
|
||||
<AppConfigProvider
|
||||
retryOptions={appConfigRetryOptions}
|
||||
{...appConfigProviderProps}
|
||||
>
|
||||
<ScarfTrackingInitializer />
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<AppInitializer />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Button } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||
import { useBackendHealth } from '@app/hooks/useBackendHealth';
|
||||
|
||||
export interface OperationButtonProps {
|
||||
onClick?: () => void;
|
||||
@@ -31,8 +33,14 @@ const OperationButton = ({
|
||||
'data-tour': dataTour
|
||||
}: OperationButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isHealthy, message: backendMessage } = useBackendHealth();
|
||||
const blockedByBackend = !isHealthy;
|
||||
const combinedDisabled = disabled || blockedByBackend;
|
||||
const tooltipLabel = blockedByBackend
|
||||
? (backendMessage ?? t('backendHealth.checking', 'Checking backend status...'))
|
||||
: null;
|
||||
|
||||
return (
|
||||
const button = (
|
||||
<Button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
@@ -41,7 +49,7 @@ const OperationButton = ({
|
||||
ml='md'
|
||||
mt={mt}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
disabled={combinedDisabled}
|
||||
variant={variant}
|
||||
color={color}
|
||||
data-testid={dataTestId}
|
||||
@@ -54,6 +62,16 @@ const OperationButton = ({
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (tooltipLabel) {
|
||||
return (
|
||||
<Tooltip content={tooltipLabel} position="top" arrow>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export default OperationButton;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
/**
|
||||
@@ -41,6 +41,8 @@ export interface AppConfig {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
|
||||
|
||||
interface AppConfigContextValue {
|
||||
config: AppConfig | null;
|
||||
loading: boolean;
|
||||
@@ -59,26 +61,42 @@ const AppConfigContext = createContext<AppConfigContextValue | undefined>({
|
||||
* Provider component that fetches and provides app configuration
|
||||
* Should be placed at the top level of the app, before any components that need config
|
||||
*/
|
||||
export const AppConfigProvider: React.FC<{
|
||||
export interface AppConfigProviderProps {
|
||||
children: ReactNode;
|
||||
retryOptions?: AppConfigRetryOptions;
|
||||
}> = ({ children, retryOptions }) => {
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
initialConfig?: AppConfig | null;
|
||||
bootstrapMode?: AppConfigBootstrapMode;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
|
||||
children,
|
||||
retryOptions,
|
||||
initialConfig = null,
|
||||
bootstrapMode = 'blocking',
|
||||
autoFetch = true,
|
||||
}) => {
|
||||
const isBlockingMode = bootstrapMode === 'blocking';
|
||||
const [config, setConfig] = useState<AppConfig | null>(initialConfig);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [fetchCount, setFetchCount] = useState(0);
|
||||
const [hasResolvedConfig, setHasResolvedConfig] = useState(Boolean(initialConfig) && !isBlockingMode);
|
||||
const [loading, setLoading] = useState(!hasResolvedConfig);
|
||||
|
||||
const maxRetries = retryOptions?.maxRetries ?? 0;
|
||||
const initialDelay = retryOptions?.initialDelay ?? 1000;
|
||||
|
||||
const fetchConfig = async (force = false) => {
|
||||
const fetchConfig = useCallback(async (force = false) => {
|
||||
// Prevent duplicate fetches unless forced
|
||||
if (!force && fetchCount > 0) {
|
||||
console.debug('[AppConfig] Already fetched, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const shouldBlockUI = !hasResolvedConfig || isBlockingMode;
|
||||
if (shouldBlockUI) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
@@ -92,12 +110,13 @@ export const AppConfigProvider: React.FC<{
|
||||
}
|
||||
|
||||
// apiClient automatically adds JWT header if available via interceptors
|
||||
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config');
|
||||
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config', !isBlockingMode ? { suppressErrorToast: true } : undefined);
|
||||
const data = response.data;
|
||||
|
||||
console.debug('[AppConfig] Config fetched successfully:', data);
|
||||
setConfig(data);
|
||||
setFetchCount(prev => prev + 1);
|
||||
setHasResolvedConfig(true);
|
||||
setLoading(false);
|
||||
return; // Success - exit function
|
||||
} catch (err: any) {
|
||||
@@ -108,6 +127,7 @@ export const AppConfigProvider: React.FC<{
|
||||
if (status === 401) {
|
||||
console.debug('[AppConfig] 401 error - using default config (login enabled)');
|
||||
setConfig({ enableLogin: true });
|
||||
setHasResolvedConfig(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -124,20 +144,23 @@ export const AppConfigProvider: React.FC<{
|
||||
const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err);
|
||||
// On error, assume login is enabled (safe default)
|
||||
setConfig({ enableLogin: true });
|
||||
// Preserve existing config (initial default or previous fetch). If nothing is set, assume login enabled.
|
||||
setConfig((current) => current ?? { enableLogin: true });
|
||||
setHasResolvedConfig(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
}, [fetchCount, hasResolvedConfig, isBlockingMode, maxRetries, initialDelay]);
|
||||
|
||||
useEffect(() => {
|
||||
// Always try to fetch config to check if login is disabled
|
||||
// The endpoint should be public and return proper JSON
|
||||
fetchConfig();
|
||||
}, []);
|
||||
if (autoFetch) {
|
||||
fetchConfig();
|
||||
}
|
||||
}, [autoFetch, fetchConfig]);
|
||||
|
||||
// Listen for JWT availability (triggered on login/signup)
|
||||
useEffect(() => {
|
||||
@@ -149,7 +172,7 @@ export const AppConfigProvider: React.FC<{
|
||||
|
||||
window.addEventListener('jwt-available', handleJwtAvailable);
|
||||
return () => window.removeEventListener('jwt-available', handleJwtAvailable);
|
||||
}, []);
|
||||
}, [fetchConfig]);
|
||||
|
||||
const value: AppConfigContextValue = {
|
||||
config,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ResponseHandler } from '@app/utils/toolResponseProcessor';
|
||||
import { createChildStub, generateProcessedFileMetadata } from '@app/contexts/file/fileActions';
|
||||
import { ToolOperation } from '@app/types/file';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { ensureBackendReady } from '@app/services/backendReadinessGuard';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { ProcessingProgress, ResponseHandler };
|
||||
@@ -187,6 +188,12 @@ export const useToolOperation = <TParams>(
|
||||
return;
|
||||
}
|
||||
|
||||
const backendReady = await ensureBackendReady();
|
||||
if (!backendReady) {
|
||||
actions.setError(t('backendHealth.offline', 'Embedded backend is offline. Please try again shortly.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
actions.setLoading(true);
|
||||
actions.setError(null);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { BackendHealthState } from '@app/types/backendHealth';
|
||||
|
||||
export function useBackendHealth(): BackendHealthState {
|
||||
return {
|
||||
status: 'healthy',
|
||||
message: null,
|
||||
isChecking: false,
|
||||
lastChecked: Date.now(),
|
||||
error: null,
|
||||
isHealthy: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Default backend readiness guard (web builds do not need to wait for
|
||||
* anything outside the browser, so we always report ready).
|
||||
*/
|
||||
export async function ensureBackendReady(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type BackendStatus = 'stopped' | 'starting' | 'healthy' | 'unhealthy';
|
||||
|
||||
export interface BackendHealthState {
|
||||
status: BackendStatus;
|
||||
message?: string | null;
|
||||
lastChecked?: number;
|
||||
isChecking: boolean;
|
||||
error: string | null;
|
||||
isHealthy: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user