Files
Stirling-PDF/frontend/src/desktop/services/connectionModeService.ts
T
James BruntonandGitHub f4725b98b0 Allow desktop app to connect to selfhosted servers (#4902)
# Description of Changes
Changes the desktop app to allow connections to self-hosted servers on
first startup. This was quite involved and hit loads of CORS issues all
through the stack, but I think it's working now. This also changes the
bundled backend to spawn on an OS-decided port rather than always
spawning on `8080`, which means that the user can have other things
running on port `8080` now and the app will still work fine. There were
quite a few places that needed to be updated to decouple the app from
explicitly using `8080` and I was originally going to split those
changes out into another PR (#4939), but I couldn't get it working
independently in the time I had, so the diff here is just going to be
complex and contian two distinct changes - sorry 🙁
2025-11-20 10:03:34 +00:00

132 lines
3.8 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core';
import { fetch } from '@tauri-apps/plugin-http';
export type ConnectionMode = 'offline' | 'server';
export type ServerType = 'saas' | 'selfhosted';
export interface ServerConfig {
url: string;
server_type: ServerType;
}
export interface ConnectionConfig {
mode: ConnectionMode;
server_config: ServerConfig | null;
}
export class ConnectionModeService {
private static instance: ConnectionModeService;
private currentConfig: ConnectionConfig | null = null;
private configLoadedOnce = false;
private modeListeners = new Set<(config: ConnectionConfig) => void>();
static getInstance(): ConnectionModeService {
if (!ConnectionModeService.instance) {
ConnectionModeService.instance = new ConnectionModeService();
}
return ConnectionModeService.instance;
}
async getCurrentConfig(): Promise<ConnectionConfig> {
if (!this.configLoadedOnce) {
await this.loadConfig();
}
return this.currentConfig || { mode: 'offline', server_config: null };
}
async getCurrentMode(): Promise<ConnectionMode> {
const config = await this.getCurrentConfig();
return config.mode;
}
async getServerConfig(): Promise<ServerConfig | null> {
const config = await this.getCurrentConfig();
return config.server_config;
}
subscribeToModeChanges(listener: (config: ConnectionConfig) => void): () => void {
this.modeListeners.add(listener);
return () => {
this.modeListeners.delete(listener);
};
}
private notifyListeners() {
if (this.currentConfig) {
this.modeListeners.forEach(listener => listener(this.currentConfig!));
}
}
private async loadConfig(): Promise<void> {
try {
const config = await invoke<ConnectionConfig>('get_connection_config');
this.currentConfig = config;
this.configLoadedOnce = true;
} catch (error) {
console.error('Failed to load connection config:', error);
// Default to offline mode on error
this.currentConfig = { mode: 'offline', server_config: null };
this.configLoadedOnce = true;
}
}
async switchToOffline(): Promise<void> {
console.log('Switching to offline mode');
await invoke('set_connection_mode', {
mode: 'offline',
serverConfig: null,
});
this.currentConfig = { mode: 'offline', server_config: null };
this.notifyListeners();
console.log('Switched to offline mode successfully');
}
async switchToServer(serverConfig: ServerConfig): Promise<void> {
console.log('Switching to server mode:', serverConfig);
await invoke('set_connection_mode', {
mode: 'server',
serverConfig,
});
this.currentConfig = { mode: 'server', server_config: serverConfig };
this.notifyListeners();
console.log('Switched to server mode successfully');
}
async testConnection(url: string): Promise<boolean> {
console.log(`[ConnectionModeService] Testing connection to: ${url}`);
try {
// Test connection by hitting the health/status endpoint
const healthUrl = `${url.replace(/\/$/, '')}/api/v1/info/status`;
const response = await fetch(healthUrl, {
method: 'GET',
connectTimeout: 10000,
});
const isOk = response.ok;
console.log(`[ConnectionModeService] Server connection test result: ${isOk}`);
return isOk;
} catch (error) {
console.warn('[ConnectionModeService] Server connection test failed:', error);
return false;
}
}
async isFirstLaunch(): Promise<boolean> {
try {
const result = await invoke<boolean>('is_first_launch');
return result;
} catch (error) {
console.error('Failed to check first launch:', error);
return false;
}
}
}
export const connectionModeService = ConnectionModeService.getInstance();