"use client"; import { useState, useRef } from "react"; import { motion } from "framer-motion"; import { Activity, Download, Upload, Server, RefreshCw } from "lucide-react"; import { CyberButton } from "../ui/cyber-button"; import { cn } from "@/lib/utils"; type TestStatus = | "idle" | "pinging" | "downloading" | "uploading" | "done" | "error"; export function SpeedtestClient() { const [status, setStatus] = useState("idle"); const [metrics, setMetrics] = useState({ ping: 0, download: 0.0, upload: 0.0, }); // Ref для принудительной отмены зависших тестов const abortControllerRef = useRef(null); const getProxyUrl = () => { return process.env.NODE_ENV === "production" ? "/api/proxy" : "http://localhost:3001"; }; const runTest = async () => { const baseUrl = getProxyUrl(); setStatus("pinging"); setMetrics({ ping: 0, download: 0, upload: 0 }); const controller = new AbortController(); abortControllerRef.current = controller; const signal = controller.signal; try { // 1. Замер PING (RTT) let totalPing = 0; let pingSuccessCount = 0; for (let i = 0; i < 5; i++) { const start = performance.now(); try { await fetch(`${baseUrl}/api/speedtest/ping?nc=${Date.now()}`, { cache: "no-store", signal, }); totalPing += performance.now() - start; pingSuccessCount++; } catch (e: any) { if (e?.name === "AbortError") throw e; // Игнорируем единичные дропы пакетов VPN при пинге } } if (pingSuccessCount === 0) throw new Error("Ping failed completely"); setMetrics((m) => ({ ...m, ping: Math.round(totalPing / pingSuccessCount), })); // 2. Замер DOWNLOAD setStatus("downloading"); const dlStart = performance.now(); const dlRes = await fetch( `${baseUrl}/api/speedtest/download?nocache=${Date.now()}`, { cache: "no-store", mode: "cors", signal }, ); const reader = dlRes.body?.getReader(); let receivedBytes = 0; const MAX_DL_TIME_MS = 10000; // Ограничиваем скачивание 10 секундами для медленных VPN if (reader) { while (true) { const { done, value } = await reader.read(); if (value) receivedBytes += value.length; const timeElapsed = performance.now() - dlStart; if (done || timeElapsed > MAX_DL_TIME_MS) { if (!done) await reader.cancel(); // Прерываем поток, если вышли за лимит времени break; } } } const dlTime = (performance.now() - dlStart) / 1000; const dlMbps = (receivedBytes * 8) / (1024 * 1024) / dlTime; setMetrics((m) => ({ ...m, download: Number(dlMbps.toFixed(2)) })); // 3. Замер UPLOAD setStatus("uploading"); const UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MB const uploadPayload = new Uint8Array(UPLOAD_SIZE); const chunk = new Uint8Array(65536); // Максимум для window.crypto // Полностью забиваем массив рандомом, иначе VPN его сожмет в ноль crypto.getRandomValues(chunk); for (let i = 0; i < UPLOAD_SIZE; i += chunk.length) { uploadPayload.set( chunk.subarray(0, Math.min(chunk.length, UPLOAD_SIZE - i)), i, ); } const ulStart = performance.now(); // Страховка от вечного зависания аплоада при обрыве туннеля const uploadTimeoutId = setTimeout(() => controller.abort(), 15000); await fetch(`${baseUrl}/api/speedtest/upload?nocache=${Date.now()}`, { method: "POST", body: uploadPayload, signal, headers: { "Content-Type": "application/octet-stream", }, }); clearTimeout(uploadTimeoutId); const ulTime = (performance.now() - ulStart) / 1000; const ulMbps = (UPLOAD_SIZE * 8) / (1024 * 1024) / ulTime; setMetrics((m) => ({ ...m, upload: Number(ulMbps.toFixed(2)) })); setStatus("done"); } catch (err: any) { if (err.name === "AbortError") { console.warn("Speedtest aborted by timeout."); } else { console.error("Speedtest error:", err); } setStatus("error"); } finally { abortControllerRef.current = null; } }; const isRunning = status !== "idle" && status !== "done" && status !== "error"; return (
{/* Декоративный фон */}

Uplink Diagnostics

Direct measurement through Netrunner MUX tunnel.

{!isRunning ? ( {" "} {status === "error" ? "RETRY TEST" : "INITIATE TEST"} ) : ( {" "} {status.toUpperCase()}... )}
{/* PING */}
Latency
{metrics.ping}{" "} ms
{/* DOWNLOAD */}
Download
{metrics.download}{" "} Mbps
{/* UPLOAD */}
Upload
{metrics.upload}{" "} Mbps
); }