216 lines
5.7 KiB
TypeScript
216 lines
5.7 KiB
TypeScript
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<string> {
|
|
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<TestResult> {
|
|
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<string>()
|
|
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<SaveResultResponse> {
|
|
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<SavedResultData> {
|
|
const res = await fetch(`${API_BASE}/results/${id}`)
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Failed to fetch result')
|
|
}
|
|
|
|
return res.json()
|
|
}
|