Skip to content

Commit 95f47bd

Browse files
Add liveness detection to sync relay connection
Send a WebSocket ping every 30s and treat a 60s gap in incoming frames as a dead connection, returning an error so the existing reconnect loop takes over. Previously a stale socket (e.g. after sleep/wake) could leave the sync task parked indefinitely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 51aed58 commit 95f47bd

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

app/src-tauri/src/adapters/nostr/relay_client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ impl SnapshotRelayConnection {
216216
}
217217
}
218218

219+
pub async fn send_ping(&mut self) -> Result<(), AppError> {
220+
self.write
221+
.send(Message::Ping(Default::default()))
222+
.await
223+
.map_err(|e| AppError::custom(format!("Failed to send websocket ping: {e}")))
224+
}
225+
219226
async fn send_json(&mut self, value: serde_json::Value) -> Result<(), AppError> {
220227
self.write
221228
.send(Message::from(value.to_string()))

app/src-tauri/src/adapters/nostr/snapshot_sync_connection.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use tokio::sync::{mpsc, watch, Mutex};
1818
use super::sync_manager::{set_state, sync_log};
1919

2020
const BLOB_RETRY_INTERVAL: Duration = Duration::from_secs(30);
21+
const RELAY_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(60);
22+
const PING_INTERVAL: Duration = Duration::from_secs(30);
2123

2224
pub(super) async fn run_snapshot_sync_connection(
2325
app: &AppHandle,
@@ -100,6 +102,8 @@ pub(super) async fn run_snapshot_sync_connection(
100102
let debounce_duration = Duration::from_secs(2);
101103
let mut next_blob_retry_at = tokio::time::Instant::now() + BLOB_RETRY_INTERVAL;
102104
let mut connected = true;
105+
let mut ping_interval = tokio::time::interval(PING_INTERVAL);
106+
ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
103107

104108
loop {
105109
let next_note_wake = pending_pushes.values().min().copied();
@@ -123,10 +127,31 @@ pub(super) async fn run_snapshot_sync_connection(
123127
continue;
124128
}
125129
_ = shutdown_rx.changed() => return Ok(()),
126-
incoming = connection.recv_message() => {
127-
handle_snapshot_incoming_message(app, incoming?, &keys, &relay_url, state, &mut connected).await?;
130+
_ = ping_interval.tick() => {
131+
if let Err(error) = connection.send_ping().await {
132+
sync_log(app, &format!("ping error: {error}"));
133+
return Err(error);
134+
}
128135
continue;
129136
}
137+
incoming = tokio::time::timeout(RELAY_INACTIVITY_TIMEOUT, connection.recv_message()) => {
138+
match incoming {
139+
Ok(result) => {
140+
handle_snapshot_incoming_message(app, result?, &keys, &relay_url, state, &mut connected).await?;
141+
continue;
142+
}
143+
Err(_) => {
144+
sync_log(
145+
app,
146+
&format!(
147+
"no relay activity for {}s; reconnecting",
148+
RELAY_INACTIVITY_TIMEOUT.as_secs()
149+
),
150+
);
151+
return Err(AppError::custom("Relay read timeout"));
152+
}
153+
}
154+
}
130155
}
131156

132157
let now = tokio::time::Instant::now();

0 commit comments

Comments
 (0)