import type { ApiClient, ServerInfo } from '@pipu/api-client'; /** * Приводит введённый пользователем адрес сервера к виду `https://host[:port][/path]` без завершающего слеша. * Без схемы подставляется https. Возвращает null, если адрес некорректен. */ export function normalizeServerUrl(input: string): string | null { const raw = input.trim(); if (!raw) return null; const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`; let url: URL; try { url = new URL(withScheme); } catch { return null; } if (url.protocol !== 'https:' && url.protocol !== 'http:') return null; if (!url.hostname) return null; const path = url.pathname.replace(/\/+$/, ''); return `${url.protocol}//${url.host}${path}`; } export type ServerCheck = | { status: 'ok'; info: ServerInfo } | { status: 'unsupported_app'; info: ServerInfo } | { status: 'offline'; error: string }; /** Сколько ждать ответа сервера, прежде чем считать его недоступным. */ export const SERVER_CHECK_TIMEOUT_MS = 10_000; /** * Проверяет сервер через GET /api/v1/server-info. * appVersionCode — versionCode Android-клиента; для веба не передаётся. */ export async function checkServer( client: ApiClient, appVersionCode?: number, timeoutMs = SERVER_CHECK_TIMEOUT_MS, ): Promise { // Гонка с таймером, а не только abort: не каждая реализация fetch (Hermes, полифилы) // надёжно прерывает запрос по сигналу. AbortSignal.timeout не используется — его может не быть в Hermes. const controller = new AbortController(); let timer: ReturnType | undefined; const timeout = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('timeout')); }, timeoutMs); }); try { const { data, error } = await Promise.race([ client.GET('/api/v1/server-info', { signal: controller.signal }), timeout, ]); if (!data) return { status: 'offline', error: error ? error.code : 'unknown' }; if (appVersionCode !== undefined && appVersionCode < data.min_android_version_code) { return { status: 'unsupported_app', info: data }; } return { status: 'ok', info: data }; } catch (e) { if (controller.signal.aborted) return { status: 'offline', error: 'timeout' }; return { status: 'offline', error: e instanceof Error ? e.message : String(e) }; } finally { clearTimeout(timer); } }