From a456517fbfb0ef150c30efe2f5cb950bddc960cb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 27 Mar 2026 12:36:35 -0400 Subject: [PATCH] Offers a new query method from quartz --- .../client/accessories/NostrClientQueryExt.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientQueryExt.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientQueryExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientQueryExt.kt new file mode 100644 index 0000000000..fefd81bbd2 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientQueryExt.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.withTimeoutOrNull + +suspend fun INostrClient.query( + relay: String, + filter: Filter, + timeoutMs: Long = 30_000L, +) = query(newSubId(), mapOf(RelayUrlNormalizer.normalize(relay) to listOf(filter)), timeoutMs) + +suspend fun INostrClient.query( + relay: String, + filters: List, + timeoutMs: Long = 30_000L, +) = query(newSubId(), mapOf(RelayUrlNormalizer.normalize(relay) to filters), timeoutMs) + +suspend fun INostrClient.query( + subscriptionId: String = newSubId(), + relay: String, + filters: List, + timeoutMs: Long = 30_000L, +) = query(subscriptionId, mapOf(RelayUrlNormalizer.normalize(relay) to filters), timeoutMs) + +suspend fun INostrClient.query( + relay: NormalizedRelayUrl, + filter: Filter, + timeoutMs: Long = 30_000L, +) = query(newSubId(), mapOf(relay to listOf(filter)), timeoutMs) + +suspend fun INostrClient.query( + relay: NormalizedRelayUrl, + filters: List, + timeoutMs: Long = 30_000L, +) = query(newSubId(), mapOf(relay to filters), timeoutMs) + +suspend fun INostrClient.query( + subscriptionId: String = newSubId(), + relay: NormalizedRelayUrl, + filters: List, + timeoutMs: Long = 30_000L, +) = query(subscriptionId, mapOf(relay to filters), timeoutMs) + +suspend fun INostrClient.query( + subscriptionId: String = newSubId(), + filters: Map>, + timeoutMs: Long = 30_000L, +): List { + val doneChannel = Channel(Channel.UNLIMITED) + + val events = mutableListOf() + val seenIds = mutableSetOf() + + val remaining = filters.keys.toMutableSet() + + val listener = + object : IRequestListener { + override fun onEvent( + event: Event, + isLive: Boolean, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + if (seenIds.add(event.id)) { + events.add(event) + } + } + + override fun onCannotConnect( + relay: NormalizedRelayUrl, + message: String, + forFilters: List?, + ) { + doneChannel.trySend(relay) + } + + override fun onClosed( + message: String, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + doneChannel.trySend(relay) + } + + override fun onEose( + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + doneChannel.trySend(relay) + } + } + + try { + openReqSubscription(subscriptionId, filters, listener) + + withTimeoutOrNull(timeoutMs) { + while (remaining.isNotEmpty()) { + val finished = doneChannel.receive() + remaining.remove(finished) + } + } + } finally { + close(subscriptionId) + doneChannel.close() + } + + return events.sortedWith(DefaultFeedOrderEvent) +} + +val DefaultFeedOrderEvent: Comparator = + compareByDescending { it.createdAt }.thenBy { it.id }