mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 15:07:10 +02:00
fix: handle all relays disconnecting before EOSE (stuck in LOADING bug)
Critical Edge Case Fix: Previously, when all relays disconnected before sending EOSE, the state remained stuck in LOADING because overallEoseReceived stayed false. Solution: Check if all relays are in terminal states - Terminal states: eose, error, or disconnected - If all terminal AND no overall EOSE yet, derive state from events: * No events → FAILED * Has events, all disconnected, streaming → OFFLINE * Has events, all disconnected, non-streaming → CLOSED * Some active, some terminal → PARTIAL New Test Coverage (5 tests): 1. All relays disconnect before EOSE, no events → FAILED 2. All relays disconnect before EOSE, with events (streaming) → OFFLINE 3. All relays disconnect before EOSE, with events (non-streaming) → CLOSED 4. Some EOSE, others disconnect before EOSE → PARTIAL 5. Mix of EOSE and errors, all terminal → PARTIAL This fixes the user-reported issue where disconnected relays show LOADING instead of transitioning to appropriate terminal state. Tests: 639/639 passing (added 5 new edge case tests)
This commit is contained in:
@@ -387,6 +387,148 @@ describe("deriveOverallState", () => {
|
||||
expect(state.disconnectedCount).toBe(15);
|
||||
expect(state.errorCount).toBe(5);
|
||||
});
|
||||
|
||||
it("NEW: All relays disconnect before EOSE, no events (streaming)", () => {
|
||||
// THE CRITICAL BUG: Stuck in LOADING when all relays disconnect
|
||||
const relays = new Map<string, ReqRelayState>([
|
||||
[
|
||||
"wss://relay1.com",
|
||||
{
|
||||
url: "wss://relay1.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "waiting", // Never got to receiving/eose
|
||||
eventCount: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
"wss://relay2.com",
|
||||
{
|
||||
url: "wss://relay2.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "waiting",
|
||||
eventCount: 0,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const state = deriveOverallState(relays, false, true, queryStartedAt);
|
||||
// Should be FAILED, not LOADING
|
||||
expect(state.status).toBe("failed");
|
||||
expect(state.connectedCount).toBe(0);
|
||||
expect(state.hasReceivedEvents).toBe(false);
|
||||
});
|
||||
|
||||
it("NEW: All relays disconnect before EOSE, with events (streaming)", () => {
|
||||
// Relays sent some events then disconnected before EOSE
|
||||
const relays = new Map<string, ReqRelayState>([
|
||||
[
|
||||
"wss://relay1.com",
|
||||
{
|
||||
url: "wss://relay1.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "receiving", // Was receiving
|
||||
eventCount: 5,
|
||||
},
|
||||
],
|
||||
[
|
||||
"wss://relay2.com",
|
||||
{
|
||||
url: "wss://relay2.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "receiving",
|
||||
eventCount: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const state = deriveOverallState(relays, false, true, queryStartedAt);
|
||||
// Should be OFFLINE (had events but all disconnected)
|
||||
expect(state.status).toBe("offline");
|
||||
expect(state.connectedCount).toBe(0);
|
||||
expect(state.hasReceivedEvents).toBe(true);
|
||||
});
|
||||
|
||||
it("NEW: All relays disconnect before EOSE, with events (non-streaming)", () => {
|
||||
// Same as above but non-streaming
|
||||
const relays = new Map<string, ReqRelayState>([
|
||||
[
|
||||
"wss://relay1.com",
|
||||
{
|
||||
url: "wss://relay1.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "receiving",
|
||||
eventCount: 5,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const state = deriveOverallState(relays, false, false, queryStartedAt);
|
||||
// Should be CLOSED (non-streaming completes)
|
||||
expect(state.status).toBe("closed");
|
||||
expect(state.hasReceivedEvents).toBe(true);
|
||||
});
|
||||
|
||||
it("NEW: Some relays EOSE, others disconnect before EOSE", () => {
|
||||
// Partial success before overall EOSE
|
||||
const relays = new Map<string, ReqRelayState>([
|
||||
[
|
||||
"wss://relay1.com",
|
||||
{
|
||||
url: "wss://relay1.com",
|
||||
connectionState: "connected",
|
||||
subscriptionState: "eose",
|
||||
eventCount: 10,
|
||||
},
|
||||
],
|
||||
[
|
||||
"wss://relay2.com",
|
||||
{
|
||||
url: "wss://relay2.com",
|
||||
connectionState: "disconnected",
|
||||
subscriptionState: "receiving",
|
||||
eventCount: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
"wss://relay3.com",
|
||||
{
|
||||
url: "wss://relay3.com",
|
||||
connectionState: "error",
|
||||
subscriptionState: "error",
|
||||
eventCount: 0,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const state = deriveOverallState(relays, false, true, queryStartedAt);
|
||||
// Should be PARTIAL (some succeeded, some failed, but not all terminal)
|
||||
expect(state.status).toBe("partial");
|
||||
expect(state.connectedCount).toBe(1);
|
||||
expect(state.eoseCount).toBe(1);
|
||||
});
|
||||
|
||||
it("NEW: Mix of EOSE and errors, all terminal", () => {
|
||||
const relays = new Map<string, ReqRelayState>([
|
||||
[
|
||||
"wss://relay1.com",
|
||||
{
|
||||
url: "wss://relay1.com",
|
||||
connectionState: "connected",
|
||||
subscriptionState: "eose",
|
||||
eventCount: 10,
|
||||
},
|
||||
],
|
||||
[
|
||||
"wss://relay2.com",
|
||||
{
|
||||
url: "wss://relay2.com",
|
||||
connectionState: "error",
|
||||
subscriptionState: "error",
|
||||
eventCount: 0,
|
||||
},
|
||||
],
|
||||
]);
|
||||
const state = deriveOverallState(relays, false, true, queryStartedAt);
|
||||
// All terminal (eose + error), should be PARTIAL
|
||||
expect(state.status).toBe("partial");
|
||||
expect(state.connectedCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ export function deriveOverallState(
|
||||
|
||||
const allEoseAt = overallEoseReceived ? Date.now() : undefined;
|
||||
|
||||
// Check if all relays are in terminal states (won't make further progress)
|
||||
const allRelaysTerminal = states.every(
|
||||
(s) =>
|
||||
s.subscriptionState === "eose" ||
|
||||
s.connectionState === "error" ||
|
||||
s.connectionState === "disconnected",
|
||||
);
|
||||
|
||||
// Derive status based on relay states and flags
|
||||
const status: ReqOverallStatus = (() => {
|
||||
// No relays selected yet (NIP-65 discovery in progress)
|
||||
@@ -67,6 +75,26 @@ export function deriveOverallState(
|
||||
return "failed";
|
||||
}
|
||||
|
||||
// All relays are in terminal states (done trying)
|
||||
// This handles the case where relays disconnect before EOSE
|
||||
if (allRelaysTerminal && !overallEoseReceived) {
|
||||
if (!hasReceivedEvents) {
|
||||
// All relays gave up before sending events
|
||||
return "failed";
|
||||
}
|
||||
if (!hasActiveRelays) {
|
||||
// Received events but all relays disconnected before EOSE
|
||||
if (isStreaming) {
|
||||
return "offline"; // Was trying to stream, now offline
|
||||
} else {
|
||||
return "closed"; // Non-streaming query, relays closed
|
||||
}
|
||||
}
|
||||
// Some relays still active but all others terminated
|
||||
// This is a partial success scenario
|
||||
return "partial";
|
||||
}
|
||||
|
||||
// No relays connected and no events received yet
|
||||
if (!hasActiveRelays && !hasReceivedEvents) {
|
||||
return "connecting";
|
||||
|
||||
Reference in New Issue
Block a user