import { invokeIpc } from '@/lib/api-client'; import { trackUiEvent } from './telemetry'; const HOST_API_PORT = 3210; const HOST_API_BASE = `http://127.0.0.1:${HOST_API_PORT}`; type HostApiProxyResponse = { ok?: boolean; data?: { status?: number; ok?: boolean; json?: unknown; text?: string; }; error?: { message?: string } | string; // backward compatibility fields success: boolean; status?: number; json?: unknown; text?: string; }; type HostApiProxyData = { status?: number; ok?: boolean; json?: unknown; text?: string; }; function headersToRecord(headers?: HeadersInit): Record { if (!headers) return {}; if (headers instanceof Headers) return Object.fromEntries(headers.entries()); if (Array.isArray(headers)) return Object.fromEntries(headers); return { ...headers }; } async function parseResponse(response: Response): Promise { if (!response.ok) { let message = `${response.status} ${response.statusText}`; try { const payload = await response.json() as { error?: string }; if (payload?.error) { message = payload.error; } } catch { // ignore body parse failure } throw new Error(message); } if (response.status === 204) { return undefined as T; } return await response.json() as T; } function resolveProxyErrorMessage(error: HostApiProxyResponse['error']): string { return typeof error === 'string' ? error : (error?.message || 'Host API proxy request failed'); } function parseUnifiedProxyResponse( response: HostApiProxyResponse, path: string, method: string, startedAt: number, ): T { if (!response.ok) { throw new Error(resolveProxyErrorMessage(response.error)); } const data: HostApiProxyData = response.data ?? {}; trackUiEvent('hostapi.fetch', { path, method, source: 'ipc-proxy', durationMs: Date.now() - startedAt, status: data.status ?? 200, }); if (data.status === 204) return undefined as T; if (data.json !== undefined) return data.json as T; return data.text as T; } function parseLegacyProxyResponse( response: HostApiProxyResponse, path: string, method: string, startedAt: number, ): T { if (!response.success) { throw new Error(resolveProxyErrorMessage(response.error)); } if (!response.ok) { const message = response.text || (typeof response.json === 'object' && response.json != null && 'error' in (response.json as Record) ? String((response.json as Record).error) : `HTTP ${response.status ?? 'unknown'}`); throw new Error(message); } trackUiEvent('hostapi.fetch', { path, method, source: 'ipc-proxy-legacy', durationMs: Date.now() - startedAt, status: response.status ?? 200, }); if (response.status === 204) return undefined as T; if (response.json !== undefined) return response.json as T; return response.text as T; } function shouldFallbackToBrowser(message: string): boolean { return message.includes('Invalid IPC channel: hostapi:fetch') || message.includes('window is not defined'); } export async function hostApiFetch(path: string, init?: RequestInit): Promise { const startedAt = Date.now(); const method = init?.method || 'GET'; // In Electron renderer, always proxy through main process to avoid CORS. try { const response = await invokeIpc('hostapi:fetch', { path, method, headers: headersToRecord(init?.headers), body: init?.body ?? null, }); if (typeof response?.ok === 'boolean' && 'data' in response) { return parseUnifiedProxyResponse(response, path, method, startedAt); } return parseLegacyProxyResponse(response, path, method, startedAt); } catch (error) { const message = error instanceof Error ? error.message : String(error); trackUiEvent('hostapi.fetch_error', { path, method, source: 'ipc-proxy', durationMs: Date.now() - startedAt, message, }); if (!shouldFallbackToBrowser(message)) { throw error; } } // Browser-only fallback (non-Electron environments). const response = await fetch(`${HOST_API_BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers || {}), }, }); trackUiEvent('hostapi.fetch', { path, method, source: 'browser-fallback', durationMs: Date.now() - startedAt, status: response.status, }); return parseResponse(response); } export function createHostEventSource(path = '/api/events'): EventSource { return new EventSource(`${HOST_API_BASE}${path}`); } export function getHostApiBase(): string { return HOST_API_BASE; }