Skip to main content
Hoot’s relay system manages WebSocket connections to Nostr relays, handling subscriptions, automatic reconnection, keepalive pings, and NIP-42 authentication.

Core components

RelayPool

The RelayPool manages multiple relay connections and subscriptions across all relays:
pub struct RelayPool {
    pub relays: HashMap<String, Relay>,
    pub subscriptions: HashMap<String, Subscription>,
    last_reconnect_attempt: Instant,
    last_ping: Instant,
    pending_auth_subscriptions: HashMap<String, Vec<String>>,
}
Key responsibilities:
  • Maintain WebSocket connections to multiple relays
  • Track global subscriptions that apply to all relays
  • Coordinate reconnection and keepalive operations
  • Handle NIP-42 authentication state and retry logic

Relay

Individual relay connection using the ewebsock library:
pub struct Relay {
    pub url: String,
    reader: ewebsock::WsReceiver,
    writer: ewebsock::WsSender,
    pub status: RelayStatus,
    pub auth_state: RelayAuthState,
}

pub enum RelayStatus {
    Connecting,
    Connected,
    Disconnected,
}
Each relay maintains:
  • WebSocket reader/writer pair with wake-up callbacks for UI repaints
  • Connection status tracking
  • Authentication state (challenge and authenticated keys)
From src/relay/mod.rs:37:
pub fn new_with_wakeup(
    url: impl Into<String>,
    wake_up: impl Fn() + Send + Sync + 'static,
) -> Self {
    let new_url: String = url.into();
    let (sender, reciever) =
        ewebsock::connect_with_wakeup(new_url.clone(), ewebsock::Options::default(), wake_up)
            .unwrap();

    let relay = Self {
        url: new_url,
        reader: reciever,
        writer: sender,
        status: RelayStatus::Connecting,
        auth_state: RelayAuthState::default(),
    };

    relay
}

Subscription

A subscription represents a Nostr filter set sent to relays:
pub struct Subscription {
    pub id: String,
    pub filters: Vec<Filter>,
}
Subscriptions are:
  • Assigned random 7-character alphanumeric IDs by default
  • Stored in the RelayPool and sent to all connected relays
  • Automatically resubscribed when relays reconnect

Reconnection logic

The keepalive() method runs periodically to maintain connections:
pub fn keepalive(&mut self, wake_up: impl Fn() + Send + Sync + Clone + 'static) {
    let now = Instant::now();

    // Check disconnected relays
    if now.duration_since(self.last_reconnect_attempt)
        >= Duration::from_secs(RELAY_RECONNECT_SECONDS)
    {
        for relay in self.relays.values_mut() {
            if relay.status != RelayStatus::Connected {
                relay.status = RelayStatus::Connecting;
                relay.reconnect(wake_up.clone());
            }
        }
        self.last_reconnect_attempt = now;
    }

    // Ping connected relays
    if now.duration_since(self.last_ping) >= Duration::from_secs(30) {
        for relay in self.relays.values_mut() {
            if relay.status == RelayStatus::Connected {
                relay.ping();
            }
        }
        self.last_ping = now;
    }
}
From src/relay/pool.rs:11:
  • Reconnection attempts happen every RELAY_RECONNECT_SECONDS (5 seconds)
  • Keepalive pings sent every 30 seconds to maintain connections
  • When a relay reconnects, all subscriptions are automatically resubmitted

Message handling

The relay system handles both inbound and outbound messages:

Outbound (ClientMessage)

pub enum ClientMessage {
    Event { event: Event },
    Req { subscription_id: String, filters: Vec<Filter> },
    Close { subscription_id: String },
    Auth { event: Event },
}

Inbound (RelayMessage)

pub enum RelayMessage<'a> {
    Event(&'a str, &'a str),
    OK(CommandResult<'a>),
    Eose(&'a str),
    Closed(&'a str, &'a str),
    Notice(&'a str),
    Auth(&'a str),
}
The try_recv() method polls all relays for incoming messages:
pub fn try_recv(&mut self) -> Option<(String, String)> {
    let relay_urls: Vec<String> = self.relays.keys().cloned().collect();
    for relay_url in relay_urls {
        if let Some(relay) = self.relays.get_mut(&relay_url) {
            if let Some(event) = relay.try_recv() {
                use WsEvent::*;
                match event {
                    Message(message) => {
                        if let Some(msg_text) = self.handle_message(relay_url.clone(), message)
                        {
                            return Some((relay_url, msg_text));
                        }
                    }
                    Opened => {
                        // Resubscribe all subscriptions when connection opens
                        for sub in self.subscriptions.clone() {
                            // ... send subscription
                        }
                    }
                    _ => {}
                }
            }
        }
    }
    None
}

NIP-42 authentication

The relay system supports NIP-42 authentication for relays that require it:
pub struct RelayAuthState {
    pub challenge: Option<String>,
    pub authenticated_keys: HashSet<String>,
}
Authentication flow:
  1. Relay sends AUTH challenge message
  2. Challenge stored in relay’s auth_state
  3. Application creates auth event using AccountManager::create_auth_event()
  4. Auth event sent via send_auth() method
  5. Relay marks key as authenticated in authenticated_keys set
  6. Pending subscriptions that failed due to auth are retried
From src/relay/pool.rs:221:
pub fn track_pending_auth_subscription(&mut self, relay_url: &str, subscription_id: &str) {
    self.pending_auth_subscriptions
        .entry(relay_url.to_string())
        .or_default()
        .push(subscription_id.to_string());
}

pub fn take_pending_auth_subscriptions(&mut self, relay_url: &str) -> Vec<String> {
    self.pending_auth_subscriptions
        .remove(relay_url)
        .unwrap_or_default()
}

Event loop integration

The relay pool integrates with the main application event loop:
  1. Wake-up callbacks: Each relay connection has a wake-up callback that triggers UI repaints when messages arrive
  2. Non-blocking: try_recv() polls for messages without blocking
  3. Automatic resubscription: When connections open, all subscriptions are resent
  4. Status tracking: Each relay maintains connection status for UI display
The relay system is designed to be resilient, automatically handling disconnections, reconnections, and authentication without manual intervention.