diff --git a/src/components/ReqViewer.tsx b/src/components/ReqViewer.tsx index 9f69ba3..4a97bc9 100644 --- a/src/components/ReqViewer.tsx +++ b/src/components/ReqViewer.tsx @@ -1234,12 +1234,17 @@ export default function ReqViewer({ )} - {/* EOSE status */} + {/* Subscription status icon */} {reqState && ( <> - {reqState.subscriptionState === "eose" ? ( + {reqState.subscriptionState === "live" ? ( + // Live: EOSE received + actively streaming + + ) : reqState.subscriptionState === "eose" ? ( + // EOSE received, idle ) : ( + // Receiving historical or waiting (reqState.subscriptionState === "receiving" || reqState.subscriptionState === "waiting") && ( diff --git a/src/hooks/useReqTimelineEnhanced.ts b/src/hooks/useReqTimelineEnhanced.ts index cda3c4e..96e0a33 100644 --- a/src/hooks/useReqTimelineEnhanced.ts +++ b/src/hooks/useReqTimelineEnhanced.ts @@ -216,10 +216,12 @@ export function useReqTimelineEnhanced( eoseAt: Date.now(), }); - // Check if ALL relays have reached EOSE + // Check if ALL relays have reached EOSE (or beyond) + // "live" means EOSE was received and we're getting live events const allEose = Array.from(next.values()).every( (s) => s.subscriptionState === "eose" || + s.subscriptionState === "live" || s.connectionState === "error" || s.connectionState === "disconnected", ); @@ -268,10 +270,15 @@ export function useReqTimelineEnhanced( lastEventAt: now, }); } else { - // Update existing relay state + // Determine correct subscription state: + // - If EOSE already received (eoseAt exists), this is a LIVE event + // - Otherwise, we're still receiving historical events + const hasEose = state.eoseAt !== undefined; + const newSubscriptionState = hasEose ? "live" : "receiving"; + next.set(url, { ...state, - subscriptionState: "receiving", + subscriptionState: newSubscriptionState, eventCount: state.eventCount + 1, firstEventAt: state.firstEventAt ?? now, lastEventAt: now, diff --git a/src/lib/req-state-machine.test.ts b/src/lib/req-state-machine.test.ts index cbe13c9..2b5b9e8 100644 --- a/src/lib/req-state-machine.test.ts +++ b/src/lib/req-state-machine.test.ts @@ -538,6 +538,7 @@ describe("getStatusText", () => { connectedCount: 3, receivingCount: 2, eoseCount: 1, + liveCount: 0, errorCount: 0, disconnectedCount: 0, hasReceivedEvents: true, @@ -568,6 +569,7 @@ describe("getStatusTooltip", () => { connectedCount: 3, receivingCount: 2, eoseCount: 1, + liveCount: 0, errorCount: 0, disconnectedCount: 0, hasReceivedEvents: true, @@ -625,7 +627,18 @@ describe("shouldAnimate", () => { }); describe("getRelayStateBadge", () => { - it("should return receiving badge", () => { + it("should return live badge (EOSE received + streaming)", () => { + const badge = getRelayStateBadge({ + url: "wss://relay.com", + connectionState: "connected", + subscriptionState: "live", + eventCount: 15, + }); + expect(badge?.text).toBe("LIVE"); + expect(badge?.color).toBe("text-success"); + }); + + it("should return receiving badge (before EOSE)", () => { const badge = getRelayStateBadge({ url: "wss://relay.com", connectionState: "connected", @@ -633,7 +646,8 @@ describe("getRelayStateBadge", () => { eventCount: 5, }); expect(badge?.text).toBe("RECEIVING"); - expect(badge?.color).toBe("text-success"); + // Warning color because it's still loading (pre-EOSE) + expect(badge?.color).toBe("text-warning"); }); it("should return eose badge", () => { diff --git a/src/lib/req-state-machine.ts b/src/lib/req-state-machine.ts index 62432a9..5476669 100644 --- a/src/lib/req-state-machine.ts +++ b/src/lib/req-state-machine.ts @@ -35,6 +35,7 @@ export function deriveOverallState( (s) => s.subscriptionState === "receiving", ).length; const eoseCount = states.filter((s) => s.subscriptionState === "eose").length; + const liveCount = states.filter((s) => s.subscriptionState === "live").length; const errorCount = states.filter((s) => s.connectionState === "error").length; const disconnectedCount = states.filter( (s) => s.connectionState === "disconnected", @@ -55,10 +56,12 @@ export function deriveOverallState( const allEoseAt = overallEoseReceived ? Date.now() : undefined; - // Check if all relays are in terminal states (won't make further progress) + // Check if all relays are in terminal states (won't make further progress on initial load) + // "live" means EOSE received + streaming, so it's also a terminal state for initial load const allRelaysTerminal = states.every( (s) => s.subscriptionState === "eose" || + s.subscriptionState === "live" || s.connectionState === "error" || s.connectionState === "disconnected", ); @@ -143,6 +146,7 @@ export function deriveOverallState( connectedCount, receivingCount, eoseCount, + liveCount, errorCount, disconnectedCount, hasReceivedEvents, @@ -242,11 +246,17 @@ export function getRelayStateBadge( ): { text: string; color: string } | null { const { subscriptionState, connectionState } = relay; - // Prioritize subscription state + // Prioritize subscription state (order matters for display priority) + if (subscriptionState === "live") { + // EOSE received, actively receiving live events + return { text: "LIVE", color: "text-success" }; + } if (subscriptionState === "receiving") { - return { text: "RECEIVING", color: "text-success" }; + // Receiving historical events (before EOSE) + return { text: "RECEIVING", color: "text-warning" }; } if (subscriptionState === "eose") { + // EOSE received, idle (no live events yet) return { text: "EOSE", color: "text-info" }; } if (subscriptionState === "error") { diff --git a/src/types/req-state.ts b/src/types/req-state.ts index dae05bb..87f131d 100644 --- a/src/types/req-state.ts +++ b/src/types/req-state.ts @@ -18,11 +18,23 @@ export type RelayConnectionState = /** * Subscription state specific to this REQ + * + * State machine: + * waiting → receiving → eose → live + * ↘ ↗ + * error + * + * - waiting: Connected, subscription sent, no events yet + * - receiving: Getting historical events (before EOSE) + * - eose: EOSE received, no live events yet + * - live: EOSE received AND receiving live events (streaming mode) + * - error: Subscription error occurred */ export type RelaySubscriptionState = | "waiting" // Connected but no events yet - | "receiving" // Events being received - | "eose" // EOSE received (real or timeout) + | "receiving" // Getting historical events (before EOSE) + | "eose" // EOSE received, idle (no live events yet) + | "live" // EOSE received AND receiving live events | "error"; // Subscription error /** @@ -75,7 +87,8 @@ export interface ReqOverallState { totalRelays: number; connectedCount: number; receivingCount: number; - eoseCount: number; + eoseCount: number; // Relays in "eose" state (EOSE received, idle) + liveCount: number; // Relays in "live" state (EOSE received + streaming) errorCount: number; disconnectedCount: number;