Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 69 additions & 30 deletions src/app/components/RelayerStatusTable.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
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 {
relayers?: Relayer[];
}

export default function RelayerStatusTable({ relayers = [] }: RelayerStatusTableProps) {
const [pingStatuses, setPingStatuses] = useState<Record<string, "active" | "inactive">>({});

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<string, "active" | "inactive"> = {};
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 (
<div className="w-full overflow-hidden rounded-xl border border-white/10 bg-black/40 backdrop-blur-md">
<table className="w-full table-fixed text-left text-sm text-white/80">
Expand All @@ -23,36 +59,39 @@ export default function RelayerStatusTable({ relayers = [] }: RelayerStatusTable
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{relayers.map((relayer) => (
<tr key={relayer.id} className="transition-colors hover:bg-white/[0.02]">
<td className="p-4 font-medium text-white">{relayer.name}</td>
<td className="p-4">
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-1 text-xs font-medium ${
relayer.status === 'Online'
? 'bg-[#39FF14]/10 text-[#39FF14]'
: relayer.status === 'Offline'
? 'bg-red-500/10 text-red-400'
: 'bg-yellow-500/10 text-yellow-400'
}`}
>
{relayers.map((relayer) => {
const status = resolveStatus(relayer);
return (
<tr key={relayer.id} className="transition-colors hover:bg-white/[0.02]">
<td className="p-4 font-medium text-white">{relayer.name}</td>
<td className="p-4">
<span
className={`h-1.5 w-1.5 rounded-full ${
relayer.status === 'Online'
? 'bg-[#39FF14] shadow-[0_0_8px_rgba(57,255,20,0.6)]'
: relayer.status === 'Offline'
? 'bg-red-400'
: 'bg-yellow-400'
className={`inline-flex items-center gap-1.5 rounded-full px-2 py-1 text-xs font-medium ${
status === "Online"
? "bg-[#39FF14]/10 text-[#39FF14]"
: status === "Offline"
? "bg-red-500/10 text-red-400"
: "bg-yellow-500/10 text-yellow-400"
}`}
/>
{relayer.status}
</span>
</td>
<td className="p-4 text-right font-mono text-white/70">
{relayer.latency} ms
</td>
</tr>
))}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
status === "Online"
? "bg-[#39FF14] shadow-[0_0_8px_rgba(57,255,20,0.6)]"
: status === "Offline"
? "bg-red-400"
: "bg-yellow-400"
}`}
/>
{status}
</span>
</td>
<td className="p-4 text-right font-mono text-white/70">
{relayer.latency} ms
</td>
</tr>
);
})}
{relayers.length === 0 && (
<tr>
<td colSpan={3} className="p-8 text-center text-white/40">
Expand Down
44 changes: 44 additions & 0 deletions src/app/services/relayerPingService.ts
Original file line number Diff line number Diff line change
@@ -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<PingResult> {
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<PingResult[]> {
return Promise.all(relayers.map(({ id, baseUrl }) => pingRelayer(id, baseUrl)));
}