diff --git a/apps/frontend/src/pages/hosting/index.vue b/apps/frontend/src/pages/hosting/index.vue index 783383f88..f10016177 100644 --- a/apps/frontend/src/pages/hosting/index.vue +++ b/apps/frontend/src/pages/hosting/index.vue @@ -21,7 +21,6 @@ :server-name="`${auth?.user?.username}'s server`" :out-of-stock-url="outOfStockUrl" :fetch-capacity-statuses="fetchCapacityStatuses" - :pings="regionPings" :regions="regions" :refresh-payment-methods="fetchPaymentData" :fetch-stock="fetchStock" @@ -1233,81 +1232,16 @@ const planQuery = async () => { } const regions = ref([]) -const regionPings = ref([]) - -function pingRegions() { +function fetchRegions() { client.archon.servers_v1.getRegions().then((res) => { regions.value = res - regions.value.forEach((region) => { - runPingTest(region) - }) }) } -const PING_COUNT = 20 -const PING_INTERVAL = 200 -const MAX_PING_TIME = 1000 - -const initialIndex = { - 'eu-lim': 31, -} - -function runPingTest(region, index = initialIndex[region.shortcode] ?? 1) { - if (index > (initialIndex[region.shortcode] ?? 1) + 10) { - regionPings.value.push({ - region: region.shortcode, - ping: -1, - }) - return - } - - const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest` - try { - const socket = new WebSocket(wsUrl) - const pings = [] - - socket.onopen = () => { - for (let i = 0; i < PING_COUNT; i++) { - setTimeout(() => { - socket.send(performance.now()) - }, i * PING_INTERVAL) - } - setTimeout( - () => { - socket.close() - - const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)]) - if (median) { - regionPings.value.push({ - region: region.shortcode, - ping: median, - }) - } - }, - PING_COUNT * PING_INTERVAL + MAX_PING_TIME, - ) - } - - socket.onmessage = (event) => { - pings.push(performance.now() - event.data) - } - - socket.onerror = (event) => { - console.error( - `Failed to connect pingtest WebSocket with ${wsUrl}, trying index ${index + 1}:`, - event, - ) - runPingTest(region, index + 1) - } - } catch (error) { - console.error(`Failed to connect pingtest WebSocket with ${wsUrl}:`, error) - } -} - onMounted(() => { startTyping() planQuery() - pingRegions() + fetchRegions() }) watch(customer, (newCustomer) => { diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 091c1a05c..226adcbab 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -44,6 +44,7 @@ export { clearNodeAuthState, nodeAuthState, setNodeAuthState } from './state/nod export * from './types' export { withJWTRetry } from './utils/jwt-retry' export { getNodeWebSocketUrl } from './utils/node-url' +export { pingWebSocketUrl, type WebSocketPingOptions } from './utils/pingtest' export { type ParsedSseEvent, type ParsedSseItem, diff --git a/packages/api-client/src/utils/pingtest.ts b/packages/api-client/src/utils/pingtest.ts new file mode 100644 index 000000000..f79ed1b6b --- /dev/null +++ b/packages/api-client/src/utils/pingtest.ts @@ -0,0 +1,97 @@ +export interface WebSocketPingOptions { + count?: number + intervalMs?: number + settleDelayMs?: number + timeoutMs?: number + signal?: AbortSignal +} + +export async function pingWebSocketUrl( + url: string, + options: WebSocketPingOptions = {}, +): Promise { + const count = options.count ?? 5 + const intervalMs = options.intervalMs ?? 200 + const settleDelayMs = options.settleDelayMs ?? 1000 + const timeoutMs = options.timeoutMs ?? count * intervalMs + settleDelayMs + 1000 + + if (options.signal?.aborted) return -1 + + return await new Promise((resolve) => { + const samples: number[] = [] + const timers = new Set>() + let socket: WebSocket | undefined + let settled = false + + const setTrackedTimeout = (callback: () => void, ms: number) => { + const timer = setTimeout(() => { + timers.delete(timer) + callback() + }, ms) + timers.add(timer) + return timer + } + + const cleanup = () => { + for (const timer of timers) clearTimeout(timer) + timers.clear() + options.signal?.removeEventListener('abort', abort) + + if ( + socket && + (socket.readyState === WebSocket.CONNECTING || socket.readyState === WebSocket.OPEN) + ) { + socket.close() + } + } + + const finish = (ping: number) => { + if (settled) return + settled = true + cleanup() + resolve(ping) + } + + const abort = () => finish(-1) + options.signal?.addEventListener('abort', abort, { once: true }) + + try { + socket = new WebSocket(url) + } catch { + finish(-1) + return + } + + setTrackedTimeout(() => finish(-1), timeoutMs) + + socket.onopen = () => { + for (let i = 0; i < count; i++) { + setTrackedTimeout(() => { + if (socket?.readyState === WebSocket.OPEN) { + socket.send(String(performance.now())) + } + }, i * intervalMs) + } + + setTrackedTimeout( + () => { + const ping = + samples.length > 0 + ? Math.round([...samples].sort((a, b) => a - b)[Math.floor(samples.length / 2)]) + : -1 + finish(ping) + }, + count * intervalMs + settleDelayMs, + ) + } + + socket.onmessage = (event) => { + samples.push(performance.now() - Number(event.data)) + } + + socket.onerror = () => finish(-1) + socket.onclose = () => { + if (!settled && samples.length === 0) finish(-1) + } + }) +} diff --git a/packages/ui/src/components/billing/ModrinthServersPurchaseModal.vue b/packages/ui/src/components/billing/ModrinthServersPurchaseModal.vue index bbc816afe..bdae9d75e 100644 --- a/packages/ui/src/components/billing/ModrinthServersPurchaseModal.vue +++ b/packages/ui/src/components/billing/ModrinthServersPurchaseModal.vue @@ -1,5 +1,5 @@