import { LatencyResult, TEST_NODES, IpInfo } from '@shared/types' const API_BASE = '/api' export interface IpInfoResponse { ip: string } export interface BatchMeasurementResponse { measurementId: string tracerouteId?: string | null } export interface BatchResultResponse { status: 'in-progress' | 'finished' results: Array<{ nodeId: string latency: number | null success: boolean stats?: { min: number max: number loss: number } }> resolvedAddress?: string ipInfo?: IpInfo | null } export interface TracerouteResponse { status: 'pending' | 'finished' totalHops?: number uniqueAsns?: number hasOptimizedRoute?: boolean detectedPremiumAsns?: number[] } export interface SavedNodeResult { nodeId: string latency: number | null success: boolean stats?: { min: number; max: number; loss: number } } export interface SaveResultRequest { type: 'single' | 'compare' input: { target: string } | { leftTarget: string; rightTarget: string } results: SavedNodeResult[] | { left: SavedNodeResult[]; right: SavedNodeResult[] } ipInfo?: IpInfo | { left: IpInfo | null; right: IpInfo | null } | null traceroute?: TracerouteStats | { left: TracerouteStats | null; right: TracerouteStats | null } | null } export interface SaveResultResponse { id: string shareUrl: string } export interface SavedResultData { type: 'single' | 'compare' input: { target: string } | { leftTarget: string; rightTarget: string } results: SavedNodeResult[] | { left: SavedNodeResult[]; right: SavedNodeResult[] } ipInfo?: IpInfo | { left: IpInfo | null; right: IpInfo | null } | null traceroute?: TracerouteStats | { left: TracerouteStats | null; right: TracerouteStats | 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 TracerouteStats { totalHops: number uniqueAsns: number hasOptimizedRoute: boolean detectedPremiumAsns?: number[] } export interface TestResult { resolvedAddress?: string ipInfo?: IpInfo | null traceroute?: TracerouteStats | null } export async function testAllNodes( target: string, onProgress: (result: LatencyResult) => void, onPhase?: (phase: 'init' | 'testing' | 'traceroute') => void ): Promise { onPhase?.('init') 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, tracerouteId }: BatchMeasurementResponse = await res.json() onPhase?.('testing') 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', stats: result.stats }) } } 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', stats: result.stats }) } } break } } // Fetch traceroute results if available let traceroute: TracerouteStats | null = null if (tracerouteId) { onPhase?.('traceroute') try { const trRes = await fetch(`${API_BASE}/latency/traceroute/${tracerouteId}`) if (trRes.ok) { const trData: TracerouteResponse = await trRes.json() if (trData.status === 'finished' && trData.totalHops !== undefined) { traceroute = { totalHops: trData.totalHops, uniqueAsns: trData.uniqueAsns ?? 0, hasOptimizedRoute: trData.hasOptimizedRoute ?? false, detectedPremiumAsns: trData.detectedPremiumAsns } } } } catch { // Traceroute is optional, ignore errors } } return { resolvedAddress, ipInfo, traceroute } } 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() }