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;