Files
grimoire/src/types/req-state.ts
Claude c60abe6df4 feat: implement production-grade REQ state machine with per-relay tracking
Core Infrastructure:
- Add ReqRelayState and ReqOverallState types for granular state tracking
- Implement deriveOverallState() state machine with 8 query states
- Create useReqTimelineEnhanced hook combining RelayStateManager + event tracking
- Add comprehensive unit tests (27 tests, all passing)

State Machine Logic:
- DISCOVERING: NIP-65 relay selection in progress
- CONNECTING: Waiting for first relay connection
- LOADING: Initial events loading
- LIVE: Streaming with active relays (only when actually connected!)
- PARTIAL: Some relays ok, some failed/disconnected
- OFFLINE: All relays disconnected after being live
- CLOSED: Query completed, all relays closed
- FAILED: All relays failed to connect

UI Updates:
- Single-word status indicators with detailed tooltips
- Condensed relay status into NIP-65 section (no duplicate lists)
- Per-relay subscription state badges (RECEIVING, EOSE, ERROR, OFFLINE)
- Event counts per relay
- Connection + Auth status integrated into single dropdown

Fixes Critical Bug:
- Solves "LIVE with 0 relays" issue (Scenario 5 from analysis)
- Distinguishes real EOSE from relay disconnections
- Accurate status for all 7 edge cases documented in analysis

Technical Approach:
- Hybrid: RelayStateManager for connections + event._relay for tracking
- Works around applesauce-relay catchError bug without forking
- No duplicate subscriptions
- Production-quality error handling

Tests: 27/27 passing including edge case scenarios
2025-12-22 16:18:15 +00:00

92 lines
2.3 KiB
TypeScript

/**
* Types for REQ subscription state tracking
*
* Provides per-relay and overall state for REQ subscriptions to enable
* accurate status indicators that distinguish between EOSE, disconnection,
* timeout, and error states.
*/
/**
* Connection state from RelayStateManager
*/
export type RelayConnectionState =
| "pending" // Not yet attempted
| "connecting" // Connection in progress
| "connected" // WebSocket connected
| "disconnected" // Disconnected (expected or unexpected)
| "error"; // Connection error
/**
* Subscription state specific to this REQ
*/
export type RelaySubscriptionState =
| "waiting" // Connected but no events yet
| "receiving" // Events being received
| "eose" // EOSE received (real or timeout)
| "error"; // Subscription error
/**
* Per-relay state for a single REQ subscription
*/
export interface ReqRelayState {
url: string;
// Connection state (from RelayStateManager)
connectionState: RelayConnectionState;
// Subscription state (tracked by enhanced hook)
subscriptionState: RelaySubscriptionState;
// Event tracking
eventCount: number;
firstEventAt?: number;
lastEventAt?: number;
// Timing
connectedAt?: number;
eoseAt?: number;
disconnectedAt?: number;
// Error handling
errorMessage?: string;
errorType?: "connection" | "protocol" | "timeout" | "auth";
}
/**
* Overall query state derived from individual relay states
*/
export type ReqOverallStatus =
| "discovering" // Selecting relays (NIP-65)
| "connecting" // Waiting for first relay to connect
| "loading" // Loading initial events
| "live" // Streaming after EOSE, relays connected
| "partial" // Some relays ok, some failed
| "closed" // All relays completed and closed
| "failed" // All relays failed
| "offline"; // All relays disconnected after being live
/**
* Aggregated state for the entire query
*/
export interface ReqOverallState {
status: ReqOverallStatus;
// Relay counts
totalRelays: number;
connectedCount: number;
receivingCount: number;
eoseCount: number;
errorCount: number;
disconnectedCount: number;
// Timing
queryStartedAt: number;
firstEventAt?: number;
allEoseAt?: number;
// Flags
hasReceivedEvents: boolean;
hasActiveRelays: boolean;
allRelaysFailed: boolean;
}