import { LatencyResult, TEST_NODES, IpInfo } from '@shared/types' const API_BASE = '/api' export interface IpInfoResponse { ip: string } export interface BatchMeasurementResponse { measurementId: string } export interface BatchResultResponse { status: 'in-progress' | 'finished' results: Array<{ nodeId: string latency: number | null success: boolean }> resolvedAddress?: string ipInfo?: IpInfo | null } export interface SaveResultRequest { type: 'single' | 'compare' input: { target: string } | { leftTarget: string; rightTarget: string } results: Array<{ nodeId: string; latency: number | null; success: boolean }> | { left: Array<{ nodeId: string; latency: number | null; success: boolean }> right: Array<{ nodeId: string; latency: number | null; success: boolean }> } ipInfo?: IpInfo | { left: IpInfo | null; right: IpInfo | null } | null } export interface SaveResultResponse { id: string shareUrl: string } export interface SavedResultData { type: 'single' | 'compare' input: { target: string } | { leftTarget: string; rightTarget: string } results: Array<{ nodeId: string; latency: number | null; success: boolean }> | { left: Array<{ nodeId: string; latency: number | null; success: boolean }> right: Array<{ nodeId: string; latency: number | null; success: boolean }> } ipInfo?: IpInfo | { left: IpInfo | null; right: IpInfo | null } | null createdAt: string } export async function fetchUserIp(): Promise { const res = await fetch(`${API_BASE}/ip`) if (!res.ok) throw new Error('Failed to fetch IP') const data: IpInfoResponse = await res.json() return data.ip } export interface TestResult { resolvedAddress?: string ipInfo?: IpInfo | null } export async function testAllNodes( target: string, onProgress: (result: LatencyResult) => void ): Promise { for (const node of TEST_NODES) { onProgress({ nodeId: node.id, latency: null, status: 'pending' }) } const res = await fetch(`${API_BASE}/latency/batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target }), }) if (!res.ok) { for (const node of TEST_NODES) { onProgress({ nodeId: node.id, latency: null, status: 'failed' }) } return {} } const { measurementId }: BatchMeasurementResponse = await res.json() for (const node of TEST_NODES) { onProgress({ nodeId: node.id, latency: null, status: 'testing' }) } const startTime = Date.now() const timeout = 60000 const completedNodes = new Set() let resolvedAddress: string | undefined let ipInfo: IpInfo | null | undefined while (Date.now() - startTime < timeout) { await new Promise(r => setTimeout(r, 800)) const pollRes = await fetch(`${API_BASE}/latency/batch/${measurementId}`) if (!pollRes.ok) continue const data: BatchResultResponse = await pollRes.json() // Capture resolved IP address if (!resolvedAddress && data.resolvedAddress) { resolvedAddress = data.resolvedAddress } // Capture IP info when available if (!ipInfo && data.ipInfo) { ipInfo = data.ipInfo } for (const result of data.results) { if (result.success && !completedNodes.has(result.nodeId)) { completedNodes.add(result.nodeId) onProgress({ nodeId: result.nodeId, latency: result.latency, status: 'success' }) } } if (data.status === 'finished') { for (const result of data.results) { if (!completedNodes.has(result.nodeId)) { onProgress({ nodeId: result.nodeId, latency: result.latency, status: result.success ? 'success' : 'failed' }) } } break } } return { resolvedAddress, ipInfo } } export async function saveResult(data: SaveResultRequest): Promise { const res = await fetch(`${API_BASE}/results`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (!res.ok) { throw new Error('Failed to save result') } return res.json() } export async function fetchSavedResult(id: string): Promise { const res = await fetch(`${API_BASE}/results/${id}`) if (!res.ok) { throw new Error('Failed to fetch result') } return res.json() }