diff --git a/src/app/components/RelayerStatusTable.tsx b/src/app/components/RelayerStatusTable.tsx index 5c0025d..240aff7 100644 --- a/src/app/components/RelayerStatusTable.tsx +++ b/src/app/components/RelayerStatusTable.tsx @@ -1,10 +1,15 @@ -import React from 'react'; +"use client"; + +import React, { useEffect, useState } from "react"; +import { pingRelayer } from "../services/relayerPingService"; export interface Relayer { id: string; name: string; - status: 'Online' | 'Offline' | 'Syncing'; + status: "Online" | "Offline" | "Syncing"; latency: number; // in ms + /** Optional base URL for /ping health check */ + baseUrl?: string; } export interface RelayerStatusTableProps { @@ -12,6 +17,37 @@ export interface RelayerStatusTableProps { } export default function RelayerStatusTable({ relayers = [] }: RelayerStatusTableProps) { + const [pingStatuses, setPingStatuses] = useState>({}); + + useEffect(() => { + if (relayers.length === 0) return; + + async function runPings() { + const results = await Promise.all( + relayers + .filter((r) => r.baseUrl) + .map((r) => pingRelayer(r.id, r.baseUrl!)) + ); + const map: Record = {}; + for (const result of results) { + map[result.id] = result.status; + } + setPingStatuses(map); + } + + runPings(); + const interval = setInterval(runPings, 30_000); + return () => clearInterval(interval); + }, [relayers]); + + function resolveStatus(relayer: Relayer): Relayer["status"] { + if (!relayer.baseUrl) return relayer.status; + const ping = pingStatuses[relayer.id]; + if (ping === "inactive") return "Offline"; + if (ping === "active") return "Online"; + return relayer.status; + } + return (
@@ -23,36 +59,39 @@ export default function RelayerStatusTable({ relayers = [] }: RelayerStatusTable - {relayers.map((relayer) => ( - - - + + - - - ))} + > + + {status} + + + + + ); + })} {relayers.length === 0 && (
{relayer.name} - + {relayers.map((relayer) => { + const status = resolveStatus(relayer); + return ( +
{relayer.name} - {relayer.status} - - - {relayer.latency} ms -
+ {relayer.latency} ms +
diff --git a/src/app/services/relayerPingService.ts b/src/app/services/relayerPingService.ts new file mode 100644 index 0000000..eb4ce88 --- /dev/null +++ b/src/app/services/relayerPingService.ts @@ -0,0 +1,44 @@ +/** + * Relayer Ping Service — Issue #230 + * Checks if a relayer responds to /ping within 500ms. + * Relayers that fail are considered inactive and removed from the "Active" pool. + */ + +export type PingStatus = "active" | "inactive"; + +export interface PingResult { + id: string; + status: PingStatus; + latency: number | null; // ms, null if timed out +} + +const PING_TIMEOUT_MS = 500; + +/** + * Pings a single relayer endpoint. + * Returns active + latency if it responds within 500ms, otherwise inactive. + */ +export async function pingRelayer(id: string, baseUrl: string): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS); + const start = Date.now(); + + try { + const res = await fetch(`${baseUrl}/ping`, { signal: controller.signal, cache: "no-store" }); + const latency = Date.now() - start; + clearTimeout(timer); + return { id, status: res.ok ? "active" : "inactive", latency }; + } catch { + clearTimeout(timer); + return { id, status: "inactive", latency: null }; + } +} + +/** + * Pings all relayers in parallel and returns only those that are active. + */ +export async function pingAllRelayers( + relayers: Array<{ id: string; baseUrl: string }> +): Promise { + return Promise.all(relayers.map(({ id, baseUrl }) => pingRelayer(id, baseUrl))); +}