Documentation Index
Fetch the complete documentation index at: https://mintlify.com/chakanysystems/hoot/llms.txt
Use this file to discover all available pages before exploring further.
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:
- Relay sends
AUTH challenge message
- Challenge stored in relay’s
auth_state
- Application creates auth event using
AccountManager::create_auth_event()
- Auth event sent via
send_auth() method
- Relay marks key as authenticated in
authenticated_keys set
- 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:
- Wake-up callbacks: Each relay connection has a wake-up callback that triggers UI repaints when messages arrive
- Non-blocking:
try_recv() polls for messages without blocking
- Automatic resubscription: When connections open, all subscriptions are resent
- 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.