const ENV_BASE_URL = typeof import.meta !== 'undefined' && typeof import.meta.env?.VITE_DESKTOP_SERVER_URL === 'string' && import.meta.env.VITE_DESKTOP_SERVER_URL.length > 0 ? import.meta.env.VITE_DESKTOP_SERVER_URL : undefined const DEFAULT_BASE_URL = ENV_BASE_URL || 'http://127.0.0.1:3456' let baseUrl = DEFAULT_BASE_URL function getErrorMessage(status: number, body: unknown) { if (body && typeof body === 'object' && 'message' in body && typeof body.message === 'string') { return body.message } if (typeof body === 'string' && body.trim().length > 0) { return body } return `API error ${status}` } export function setBaseUrl(url: string) { baseUrl = url.replace(/\/$/, '') } export function getBaseUrl() { return baseUrl } export function getDefaultBaseUrl() { return DEFAULT_BASE_URL } export class ApiError extends Error { constructor( public status: number, public body: unknown, ) { super(getErrorMessage(status, body)) this.name = 'ApiError' } } async function request(method: string, path: string, body?: unknown, options?: { timeout?: number }): Promise { const url = `${baseUrl}${path}` const headers: Record = { 'Content-Type': 'application/json', } const controller = new AbortController() const timeoutMs = options?.timeout ?? 30_000 const timeout = setTimeout(() => controller.abort(), timeoutMs) try { const res = await fetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, signal: controller.signal, }) clearTimeout(timeout) if (!res.ok) { const errorBody = await res.json().catch(() => res.text()) throw new ApiError(res.status, errorBody) } if (res.status === 204) return undefined as T return res.json() as Promise } catch (err) { clearTimeout(timeout) if (controller.signal.aborted) { throw new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`) } throw err } } export const api = { get: (path: string, options?: { timeout?: number }) => request('GET', path, undefined, options), post: (path: string, body?: unknown, options?: { timeout?: number }) => request('POST', path, body, options), put: (path: string, body?: unknown) => request('PUT', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), delete: (path: string) => request('DELETE', path), }