Cancel pending publications when custom relays are removed

This commit is contained in:
callebtc 2026-09-08 00:50:01 +03:00
parent f41e844298
commit 994d40b46b
4 changed files with 28 additions and 7 deletions

View File

@ -79,6 +79,11 @@ internal class NostrPendingEventQueue(
}
}
fun removeRelay(relayUrl: String) = synchronized(lock) {
entries.forEach { it.pendingRelayUrls.remove(relayUrl) }
entries.removeAll { it.pendingRelayUrls.isEmpty() }
}
fun removeLiveLocationEvents() {
synchronized(lock) {
entries.removeAll { it.liveLocationToken != null }

View File

@ -83,6 +83,7 @@ class NostrRelayManager private constructor() {
private val _customRelays = MutableStateFlow<List<String>>(emptyList())
val customRelays: StateFlow<List<String>> = _customRelays.asStateFlow()
private var customRelaysLoaded = false
private val removedCustomRelayUrls = java.util.concurrent.ConcurrentHashMap.newKeySet<String>()
@Synchronized
private fun loadCustomRelays(context: android.content.Context) {
@ -101,6 +102,7 @@ class NostrRelayManager private constructor() {
if (url in defaultRelays()) return true
if (_customRelays.value.size >= 20) return false
val previousDefaults = defaultRelays().toSet()
removedCustomRelayUrls.remove(url)
_customRelays.value = _customRelays.value + url
persistCustomRelays()
nonLiveRelayUrls.add(url)
@ -116,6 +118,8 @@ class NostrRelayManager private constructor() {
@Synchronized
fun removeCustomRelay(url: String) {
if (url !in _customRelays.value) return
removedCustomRelayUrls.add(url)
messageQueue.removeRelay(url)
_customRelays.value = _customRelays.value - url
persistCustomRelays()
activeSubscriptions.replaceAll { _, subscription ->
@ -930,7 +934,7 @@ class NostrRelayManager private constructor() {
urlString: String,
liveLocationToken: Long? = null
) {
if (!desiredConnected.get()) return
if (!desiredConnected.get() || urlString in removedCustomRelayUrls) return
val connectionToken = liveLocationToken
?.takeIf { urlString !in nonLiveRelayUrls }
if (!isNetworkActionAllowed(connectionToken)) return
@ -945,6 +949,7 @@ class NostrRelayManager private constructor() {
.build()
val started = runNetworkAction(connectionToken) {
if (urlString in removedCustomRelayUrls) return@runNetworkAction
val webSocket = httpClient.newWebSocket(
request,
RelayWebSocketListener(urlString, connectionToken)
@ -952,7 +957,7 @@ class NostrRelayManager private constructor() {
val existing = connections.putIfAbsent(urlString, webSocket)
when {
existing != null -> webSocket.close(1000, "Duplicate connection")
!desiredConnected.get() -> {
!desiredConnected.get() || urlString in removedCustomRelayUrls -> {
connections.remove(urlString, webSocket)
webSocket.close(1000, "Connection no longer desired")
}
@ -973,14 +978,14 @@ class NostrRelayManager private constructor() {
liveLocationToken: Long? = null,
publicationAllowed: () -> Boolean = { true }
): Boolean {
if (!publicationAllowed() || !isNetworkActionAllowed(liveLocationToken)) return false
if (!publicationAllowed() || relayUrl in removedCustomRelayUrls || !isNetworkActionAllowed(liveLocationToken)) return false
return try {
val request = NostrRequest.Event(event)
val message = gson.toJson(request, NostrRequest::class.java)
var success = false
runNetworkAction(liveLocationToken) {
if (publicationAllowed()) success = webSocket.send(message)
if (publicationAllowed() && relayUrl !in removedCustomRelayUrls) success = webSocket.send(message)
}
if (success) {
// Update relay stats
@ -1238,7 +1243,7 @@ class NostrRelayManager private constructor() {
) : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
if (!desiredConnected.get() ||
if (!desiredConnected.get() || relayUrl in removedCustomRelayUrls ||
connections[relayUrl] !== webSocket ||
!isNetworkActionAllowed(liveLocationToken)
) {

View File

@ -82,6 +82,17 @@ class NostrPendingEventQueueTest {
assertEquals(0, queue.size())
}
@Test
fun `removing a relay drops its pending work without losing other deliveries`() {
val queue = NostrPendingEventQueue(4)
queue.enqueue(event("shared"), listOf("removed", "retained"), null)
queue.enqueue(event("removed-only"), listOf("removed"), null)
queue.removeRelay("removed")
assertEquals(0, queue.pendingForRelay("removed").size)
assertEquals(listOf("shared"), queue.pendingForRelay("retained").map { it.event.content })
assertEquals(1, queue.size())
}
private fun event(content: String): NostrEvent {
val privateKey = "0".repeat(63) + "1"
return NostrEvent(

View File

@ -116,8 +116,8 @@ Physical Mesh Lab and Android-to-iOS interoperability are required separately:
- One-time-prekey races and consumption persistence, plus iOS vectors on both
real clients. Static screenshots cannot establish any of these properties.
The updated panic-help sentence falls back to English where previous translations
still promised immediate erasure; those stale claims are removed. Full locale coverage, accessibility review and the physical matrix remain
The updated panic-help sentence uses the English resource fallback until translated.
Full locale coverage, accessibility review and the physical matrix remain
release work. Manual-channel Nostr board publication still obeys the existing
location-note privacy gate; this integration does not weaken it to force a
successful publish. No claim is made that this large integration is ready to