fix: improve relay state tracking and add relay type indicators

State Tracking Fixes:
- Sync connection state for ALL relays in query, not just initialized ones
- Defensively initialize missing relay states during sync
- Handle events from unknown relays (defensive initialization)
- Add debug console logs to track state transitions

Relay Type Indicators:
- Explicit relays: Blue link icon (relays specified directly)
- Outbox relays: Purple sparkles (NIP-65 selected)
- Fallback relays: Gray inbox icon (fallback when outbox incomplete)
- Each type has tooltip explaining source

This should fix:
- "0/4 relays but events coming in" bug
- "Stuck in LOADING" when events are arriving
- Missing visibility for relay source types

Tests: 634/634 passing
This commit is contained in:
Claude
2025-12-22 16:36:56 +00:00
parent c9bf2fe599
commit 70651ae29f
2 changed files with 103 additions and 23 deletions

View File

@@ -16,6 +16,9 @@ import {
Loader2,
Mail,
Send,
Inbox,
Sparkles,
Link as LinkIcon,
} from "lucide-react";
import { Virtuoso } from "react-virtuoso";
import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced";
@@ -1016,6 +1019,41 @@ export default function ReqViewer({
// Find NIP-65 info for this relay (if using outbox)
const nip65Info = reasoning?.find((r) => r.relay === url);
// Determine relay type
const relayType = relays
? "explicit" // Explicitly specified relays
: nip65Info && !nip65Info.isFallback
? "outbox" // NIP-65 outbox relay
: "fallback"; // Fallback relay
// Type indicator icon
const typeIcon = {
explicit: (
<Tooltip>
<TooltipTrigger asChild>
<LinkIcon className="size-3 text-blue-500" />
</TooltipTrigger>
<TooltipContent>Explicit relay</TooltipContent>
</Tooltip>
),
outbox: (
<Tooltip>
<TooltipTrigger asChild>
<Sparkles className="size-3 text-purple-500" />
</TooltipTrigger>
<TooltipContent>NIP-65 Outbox relay</TooltipContent>
</Tooltip>
),
fallback: (
<Tooltip>
<TooltipTrigger asChild>
<Inbox className="size-3 text-muted-foreground/60" />
</TooltipTrigger>
<TooltipContent>Fallback relay</TooltipContent>
</Tooltip>
),
}[relayType];
return (
<div
key={url}
@@ -1027,6 +1065,9 @@ export default function ReqViewer({
className="flex-1 truncate font-mono text-foreground/80"
/>
<div className="flex items-center gap-1.5 flex-shrink-0 text-muted-foreground">
{/* Relay type indicator */}
{typeIcon}
{/* Event count */}
{reqState && reqState.eventCount > 0 && (
<Tooltip>
@@ -1087,13 +1128,6 @@ export default function ReqViewer({
</Tooltip>
)}
{/* Fallback indicator */}
{nip65Info && nip65Info.isFallback && (
<span className="text-[10px] text-muted-foreground/60">
fallback
</span>
)}
{/* Auth icon */}
{authIcon && (
<Tooltip>

View File

@@ -98,29 +98,58 @@ export function useReqTimelineEnhanced(
// Sync connection states from RelayStateManager
// This runs whenever globalRelayStates updates
useEffect(() => {
if (relays.length === 0) return;
setRelayStates((prev) => {
const next = new Map(prev);
let changed = false;
for (const [url, state] of prev) {
// Sync state for all relays in our query
for (const url of relays) {
const globalState = globalRelayStates[url];
if (
globalState &&
globalState.connectionState !== state.connectionState
) {
const currentState = prev.get(url);
// Initialize if relay not in map yet (shouldn't happen, but defensive)
if (!currentState) {
next.set(url, {
...state,
url,
connectionState: globalState?.connectionState || "pending",
subscriptionState: "waiting",
eventCount: 0,
connectedAt: globalState?.lastConnected,
disconnectedAt: globalState?.lastDisconnected,
});
changed = true;
console.log(
"REQ Enhanced: Initialized missing relay state",
url,
globalState?.connectionState,
);
} else if (
globalState &&
globalState.connectionState !== currentState.connectionState
) {
// Update connection state if changed
next.set(url, {
...currentState,
connectionState: globalState.connectionState as any,
connectedAt: globalState.lastConnected,
disconnectedAt: globalState.lastDisconnected,
});
changed = true;
console.log(
"REQ Enhanced: Connection state changed",
url,
currentState.connectionState,
"→",
globalState.connectionState,
);
}
}
return changed ? next : prev;
});
}, [globalRelayStates]);
}, [globalRelayStates, relays]);
// Subscribe to events
useEffect(() => {
@@ -208,17 +237,34 @@ export function useReqTimelineEnhanced(
if (relayUrl) {
setRelayStates((prev) => {
const state = prev.get(relayUrl);
if (!state) return prev;
const now = Date.now();
const next = new Map(prev);
next.set(relayUrl, {
...state,
subscriptionState: "receiving",
eventCount: state.eventCount + 1,
firstEventAt: state.firstEventAt ?? now,
lastEventAt: now,
});
if (!state) {
// Relay not in map - initialize it (defensive)
console.warn(
"REQ Enhanced: Event from unknown relay, initializing",
relayUrl,
);
next.set(relayUrl, {
url: relayUrl,
connectionState: "connected",
subscriptionState: "receiving",
eventCount: 1,
firstEventAt: now,
lastEventAt: now,
});
} else {
// Update existing relay state
next.set(relayUrl, {
...state,
subscriptionState: "receiving",
eventCount: state.eventCount + 1,
firstEventAt: state.firstEventAt ?? now,
lastEventAt: now,
});
}
return next;
});
}