diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt index 0596190836..b77d033c7d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/INostrClient.kt @@ -57,6 +57,22 @@ interface INostrClient : AutoCloseable { */ fun resetBackoff() { } + /** + * Puts [url] back in the connection pool if it is no longer there, without dialing it. + * + * [reconnect] can only act on relays the pool still holds, so a caller that means to + * revive one it stopped hearing from has to restore that precondition first — + * otherwise the reconnect iterates past an empty pool and does nothing at all, which + * is indistinguishable from a relay that was asked and stayed silent. + * + * A relay leaves the pool when nothing wants it any more (no subscription, no count, + * no pending publish). That is normally the right call and normally permanent, so + * this is deliberately narrow: it restores membership and leaves dialing, backoff and + * filter syncing to [reconnect]. Defaults to a no-op — a client with no pool has + * nothing to restore and should not be forced to implement one. + */ + fun ensureInPool(url: NormalizedRelayUrl) { } + fun isActive(): Boolean /** 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 5b75850ea3..21c16d9b64 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 @@ -274,6 +274,12 @@ class NostrClient( relayPool.resetBackoff() } + override fun ensureInPool(url: NormalizedRelayUrl) { + // Membership only. Connecting is reconnect()'s job, and going through the pool's + // own create keeps the relay client identical to the one publish would have made. + relayPool.createRelayIfAbsent(url) + } + override fun subscribe( subId: String, filters: Map>, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientPublishExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientPublishExt.kt index 222e743746..4a4428f890 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientPublishExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientPublishExt.kt @@ -273,9 +273,20 @@ suspend fun INostrClient.publishAndCollectResults( } // The event is still in the pool's outbox for this relay, // so the dial is the whole job: the pool flushes what it - // owes the relay once the socket is back. Ignore the - // accumulated backoff — this is a user-visible publish - // waiting on it, not a background refresh. + // owes the relay once the socket is back. + // + // Except that a reconnect can only dial relays the pool + // still holds, and a relay can leave it — the desired set + // is sampled, so a snapshot taken before this publish + // claimed the relay retires it, socket and all. Restoring + // membership first is what keeps the retry from being a + // silent no-op: issued, logged, dialing nothing, and + // reported at the deadline as the hang-up we already knew + // about. A no-op when the relay is still there, which is + // the ordinary case. + ensureInPool(result.relay) + // Ignore the accumulated backoff — this is a user-visible + // publish waiting on it, not a background refresh. resetBackoff() reconnect(onlyIfChanged = false, ignoreRetryDelays = true) }