From 709e254c15b837d8a6cb33f78d6b9c59c4fa99a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 20:28:02 +0000 Subject: [PATCH] fix: periodic keep-alive to revive relays in long backoff When a relay enters a long backoff (5 min, e.g. host unreachable or a server returned an HTTP error during handshake), the per-relay delayToConnectInSeconds blocks reconnect attempts for up to 5 minutes. Without a wakeup, nothing inside NostrClient revisits that relay until the next subscribe/count/publish. Add a keep-alive coroutine that calls reconnectIfNeedsTo(false) every 60s while the client is active. The per-relay backoff still gates the actual reconnect, so dead relays are not hammered, but a relay whose backoff window has elapsed is reconnected within ~60s of becoming eligible. The job lives in scope and is cancelled by close(). --- .../nip01Core/relay/client/NostrClient.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 824158bdaf..84d2218067 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -157,6 +158,27 @@ class NostrClient( false, ) + /** + * Periodically wakes up disconnected relays. Without this, a relay + * that hit a long backoff (5 min, e.g. host unreachable or a server + * error code) would stay disconnected forever in the absence of any + * subscription change. The per-relay [BasicRelayClient] backoff still + * gates the actual reconnect attempt, so dead relays are not hammered. + */ + private val keepAliveJob = + scope.launch { + while (true) { + delay(KEEP_ALIVE_INTERVAL_MS) + if (this@NostrClient.isActive) { + relayPool.reconnectIfNeedsTo(ignoreRetryDelays = false) + } + } + } + + companion object { + private const val KEEP_ALIVE_INTERVAL_MS = 60_000L + } + override fun reconnect( onlyIfChanged: Boolean, ignoreRetryDelays: Boolean,