From a3fac4fc085a04a3da344e53c8001d4bdb3c146b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Aug 2026 12:07:27 -0400 Subject: [PATCH] Suspend the incoming-message chain down to SubscriptionListener.onEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A consumer that cannot suspend has to block, and blocking here deadlocks the whole client. Measured on a mirror built against this library, twice, ~13 minutes after each start: all 64 shared coroutine workers parked in `runBlocking` beneath `trySendBlocking`, called from the websocket message callback. The consumer draining that channel needed threads from the same pool to reach its store, so it could never make room, so the producers never woke. Every stream, the health reporter, all of it stopped, at 2% CPU with a healthy, idle backend. A full queue was the symptom; producers eating the threads the drain needed was the cause. The coroutine context was already there — BasicOkHttpWebSocket has always processed messages inside `scope.launch { for (message in incomingMessages) }` — so the only thing forcing a blocking hand-off was that the hops in between were declared non-suspend. Now they are not: WebSocketListener.onMessage RelayConnectionListener.onIncomingMessage PoolRequests/PoolCounts/PoolEventOutbox.onIncomingMessage SubscriptionListener.onEvent fetchAllPages / negentropy accessories' onEvent parameter A consumer that fills its buffer now suspends and releases its thread rather than holding it, which is the same reasoning BasicOkHttpWebSocket already documents for keeping its own channel UNLIMITED so a slow consumer cannot block OkHttp reader threads. This extends it one layer down. BLE is the one transport whose callback genuinely cannot suspend — the platform hands notifications to a plain callback — so BleNostrClient gets the same treatment the websocket transport already had: an UNLIMITED hand-off channel so the BLE stack is never blocked, drained by ONE coroutine so message order survives the boundary. Tests that drove these entry points directly now do so from `runTest`, or from `runBlocking` where the call sits inside a raw thread or Runnable that models a platform callback. Co-Authored-By: Claude Opus 5 (1M context) --- .../napplet/NappletLiveSubscriptions.kt | 2 +- .../amethyst/service/ClinkDebitPayer.kt | 2 +- .../amethyst/service/ClinkOfferPayer.kt | 2 +- .../diagnostics/BootRelayDiagnostics.kt | 2 +- .../diagnostics/DmRelayDiagnosticsLogger.kt | 2 +- .../eoseManagers/PerUniqueIdEoseManager.kt | 2 +- .../PerUserAndFollowListEoseManager.kt | 2 +- .../eoseManagers/PerUserEoseManager.kt | 2 +- .../SingleSubNoEoseCacheEoseManager.kt | 2 +- .../notifyCommand/model/NotifyCoordinator.kt | 2 +- .../AccountFollowsLoaderSubAssembler.kt | 2 +- .../AccountNotificationsHistoryEoseManager.kt | 2 +- .../NwcNotificationsEoseManager.kt | 2 +- .../AccountGiftWrapsHistoryEoseManager.kt | 2 +- .../user/watchers/UserWatcherSubAssembler.kt | 2 +- .../speedLogger/RelaySpeedLogger.kt | 2 +- .../resourceusage/RelayUsageListener.kt | 2 +- .../ChatroomNip04HistorySubAssembler.kt | 2 +- .../ConcordChannelHistoryFilterAssembler.kt | 2 +- ...elayGroupOpenChatHistoryFilterAssembler.kt | 2 +- ...yGroupOpenThreadsHistoryFilterAssembler.kt | 2 +- .../ChatroomListNip04HistorySubAssembler.kt | 2 +- .../datasource/ChessFeedFilterSubAssembler.kt | 2 +- .../loggedIn/relays/eventsync/EventSync.kt | 2 +- .../com/vitorpamplona/amethyst/cli/Context.kt | 2 +- .../amethyst/cli/commands/GeochatCommands.kt | 2 +- .../amethyst/cli/commands/NipCommand.kt | 2 +- .../amethyst/cli/commands/NostrConnect.kt | 2 +- .../amethyst/cli/commands/SubscribeCommand.kt | 2 +- .../nip64Chess/ChessRelayFetchHelper.kt | 2 +- .../assemblers/FeedMetadataCoordinator.kt | 6 +- .../eoseManagers/PerKeyEoseManager.kt | 2 +- .../eoseManagers/SingleSubEoseManager.kt | 2 +- .../relays/health/RelayHealthListener.kt | 2 +- .../service/broadcast/BroadcastTracker.kt | 4 +- .../amethyst/commons/wot/OutboxDispatcher.kt | 4 +- .../nip64Chess/ChessEventBroadcaster.kt | 2 +- .../relayClient/paging/WindowLoadTracker.kt | 2 +- .../relays/health/RelayLatencyListener.kt | 2 +- .../nip17Dm/DmInboxRelayResolverOutboxTest.kt | 2 +- .../commons/wot/OutboxDispatcherTest.kt | 2 +- .../vitorpamplona/amethyst/desktop/Main.kt | 4 +- .../desktop/account/AccountManager.kt | 2 +- .../desktop/followpacks/FollowPacksState.kt | 2 +- .../desktop/followpacks/MetadataPrefetch.kt | 2 +- .../desktop/followpacks/ui/FromThePackFeed.kt | 2 +- .../followpacks/ui/RenderFollowPackCard.kt | 2 +- .../desktop/network/RelayConnectionManager.kt | 4 +- .../search/DesktopRelayUserSearchDelegate.kt | 2 +- .../subscriptions/ChessSubscription.kt | 2 +- .../DesktopRelaySubscriptionsCoordinator.kt | 4 +- .../subscriptions/SubscriptionUtils.kt | 2 +- .../desktop/ui/ImportFollowListDialog.kt | 4 +- .../amethyst/desktop/ui/NoteActions.kt | 4 +- .../desktop/ui/chats/ChatroomListState.kt | 2 +- .../desktop/benchmark/LaunchScenario.kt | 2 +- .../testrelay/LaunchFixtureRelayTest.kt | 2 +- .../testrelay/SubscribeBeforeConnectTest.kt | 2 +- .../geode/mirror/MirrorWorker.kt | 2 +- .../geode/GracefulShutdownTest.kt | 2 +- .../com/vitorpamplona/geode/KtorRelayTest.kt | 2 +- .../geode/Nip01ComplianceTest.kt | 12 +- .../vitorpamplona/geode/Nip09DeletionTest.kt | 2 +- .../geode/Nip77NegentropyTest.kt | 2 +- .../mirror/MirrorWorkerTrustOriginTest.kt | 2 +- .../vitorpamplona/geode/perf/LoadBenchmark.kt | 6 +- .../geode/testing/SubscriptionTesting.kt | 2 +- .../graperank/GrapeRankCrawler.kt | 2 +- .../nip01Core/relay/client/NostrClient.kt | 2 +- .../accessories/AdaptiveRelayLimiter.kt | 2 +- .../client/accessories/EventCollector.kt | 2 +- .../client/accessories/NostrClientCountExt.kt | 4 +- .../NostrClientFetchAllPagesExt.kt | 6 +- .../NostrClientFetchAllWithHooksExt.kt | 2 +- .../accessories/NostrClientFetchFirstExt.kt | 2 +- .../NostrClientNegentropyFanOutExt.kt | 2 +- .../NostrClientNegentropySyncExt.kt | 14 +- .../accessories/NostrClientPublishExt.kt | 2 +- .../RelayInsertConfirmationCollector.kt | 2 +- .../relay/client/accessories/RelayLogger.kt | 2 +- .../relay/client/accessories/RelayNotifier.kt | 2 +- .../relay/client/auth/RelayAuthenticator.kt | 2 +- .../client/counts/RelayActiveCountStates.kt | 2 +- .../relay/client/limits/RelayLimitsTracker.kt | 2 +- .../listeners/RedirectConnectionListener.kt | 2 +- .../listeners/RelayConnectionListener.kt | 2 +- .../nip01Core/relay/client/pool/PoolCounts.kt | 2 +- .../relay/client/pool/PoolEventOutbox.kt | 2 +- .../relay/client/pool/PoolRequests.kt | 2 +- .../nip01Core/relay/client/pool/RelayPool.kt | 2 +- .../relay/client/reqs/DynamicSubscription.kt | 2 +- .../client/reqs/NostrClientFetchAsFlowExt.kt | 2 +- .../reqs/NostrClientSubscribeAsFlowExt.kt | 2 +- .../client/reqs/RelayActiveRequestStates.kt | 2 +- .../relay/client/reqs/StaticSubscription.kt | 2 +- .../relay/client/reqs/SubscriptionListener.kt | 2 +- .../relay/client/reqs/stats/RelayReqStats.kt | 2 +- .../client/single/basic/BasicRelayClient.kt | 2 +- .../standalone/StandaloneRelayClient.kt | 2 +- .../relay/client/stats/RelayStats.kt | 2 +- .../relay/sockets/WebSocketListener.kt | 2 +- .../server/NostrConnectSignerService.kt | 2 +- .../reachability/RelayObserver.kt | 2 +- .../reachability/RelayProber.kt | 2 +- .../nip77Negentropy/NegentropyManager.kt | 2 +- .../quartz/nipBEBle/relay/BleMeshManager.kt | 2 +- .../quartz/nipBEBle/relay/BleNostrClient.kt | 40 +- .../client/limits/RelayLimitsTrackerTest.kt | 93 +++-- .../client/pool/PoolEventOutboxAuthTest.kt | 106 ++--- .../client/pool/PoolRequestsRefusalTest.kt | 194 ++++----- .../inprocess/InProcessWebSocketTest.kt | 4 +- .../server/NostrConnectSignerServiceTest.kt | 2 +- .../reachability/RelayObserverTest.kt | 390 +++++++++--------- .../reachability/RelayProberFlowTest.kt | 178 ++++---- .../relay/NostrClientManualSubTest.kt | 2 +- .../relay/NostrClientRepeatSubTest.kt | 2 +- .../relay/PoolRequestsConcurrencyTest.kt | 109 ++--- .../RelayAuthenticatorReauthOnClosedTest.kt | 2 +- .../client/NegentropyRejectionFallbackTest.kt | 41 +- .../relay/prodbench/ByIdFetchBenchmark.kt | 2 +- .../relay/prodbench/DispatchStageBenchmark.kt | 19 +- .../prodbench/NegentropyMultiRelayLiveTest.kt | 2 +- .../relay/prodbench/NegentropyStallRepro.kt | 2 +- .../prodbench/ProductionReceiverBenchmark.kt | 2 +- 124 files changed, 770 insertions(+), 682 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLiveSubscriptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLiveSubscriptions.kt index 8429a0b721..1e33c16323 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLiveSubscriptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLiveSubscriptions.kt @@ -80,7 +80,7 @@ class NappletLiveSubscriptions { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt index 664b55ed61..3ea9d1678d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt @@ -113,7 +113,7 @@ object ClinkDebitPayer { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt index 28a0d64fb0..a5911dc6c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt @@ -85,7 +85,7 @@ object ClinkOfferPayer { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/BootRelayDiagnostics.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/BootRelayDiagnostics.kt index 1966bac786..054f6f855e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/BootRelayDiagnostics.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/BootRelayDiagnostics.kt @@ -158,7 +158,7 @@ class BootRelayDiagnostics( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt index 4aefae6ba6..07f13fbd0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt @@ -112,7 +112,7 @@ class DmRelayDiagnosticsLogger( Log.d(TAG) { "[+${at()}ms] REQ -> ${relay.url.url} success=$success ${cmdStr.take(400)}" } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 0427380423..61c297887e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -78,7 +78,7 @@ abstract class PerUniqueIdEoseManager( newEose(key, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index cc55f02d7a..7f53cbd0aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -90,7 +90,7 @@ abstract class PerUserAndFollowListEoseManager( newEose(key, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index b904bf4ab9..c89d53a6e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -77,7 +77,7 @@ abstract class PerUserEoseManager( newEose(key, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index d4df1e5deb..a80357e283 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -53,7 +53,7 @@ abstract class SingleSubNoEoseCacheEoseManager( } } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt index 92b40d456e..c7e146b5bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt @@ -78,7 +78,7 @@ class NotifyCoordinator( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt index 08ec932e0e..dcda63f127 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/follows/AccountFollowsLoaderSubAssembler.kt @@ -116,7 +116,7 @@ class AccountFollowsLoaderSubAssembler( newEose(TimeUtils.now(), relay, forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsHistoryEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsHistoryEoseManager.kt index 485f8fb9f9..5f15938130 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsHistoryEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsHistoryEoseManager.kt @@ -193,7 +193,7 @@ class AccountNotificationsHistoryEoseManager( // cursors so a late callback can't move another account's cursors. newEose runs regardless. val myCursors = key.account.notificationHistory return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip47WalletConnect/NwcNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip47WalletConnect/NwcNotificationsEoseManager.kt index 384c89df06..97098d765e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip47WalletConnect/NwcNotificationsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip47WalletConnect/NwcNotificationsEoseManager.kt @@ -124,7 +124,7 @@ class NwcNotificationsEoseManager( newEose(key, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt index 02d351be88..911e6fb132 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsHistoryEoseManager.kt @@ -115,7 +115,7 @@ class AccountGiftWrapsHistoryEoseManager( // cursors so a late callback can't move another account's cursors. newEose runs regardless. val myCursors = key.account.chatroomList.giftWrapHistory return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index d695cd8e7e..f5381a9be1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -74,7 +74,7 @@ class UserWatcherSubAssembler( newEose(relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt index 66718a9ea1..32942920eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt @@ -42,7 +42,7 @@ class RelaySpeedLogger( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt index a8ba773138..5d97ef56cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt @@ -48,7 +48,7 @@ class RelayUsageListener( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt index 4cbe77bffb..c9e30794a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomNip04HistorySubAssembler.kt @@ -116,7 +116,7 @@ class ChatroomNip04HistorySubAssembler( // so a late callback can't move another room's cursors. newEose (framework bookkeeping) runs anyway. val myCursors = cursorsFor(key) return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt index 2720cb1d77..a6cb65f165 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelHistoryFilterAssembler.kt @@ -170,7 +170,7 @@ class ConcordChannelHistorySubAssembler( // cursors so a late callback can't move another channel's cursors. newEose runs regardless. val myCursors = cursorsFor(key) return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt index 7b6f7bbcac..f75b814bf7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatHistoryFilterAssembler.kt @@ -126,7 +126,7 @@ class RelayGroupOpenChatHistorySubAssembler( // cursors so a late callback can't move another group's cursors. newEose runs regardless. val myCursors = cursorsFor(key) return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt index ddbef10a9a..cb2d8cc4b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler.kt @@ -123,7 +123,7 @@ class RelayGroupOpenThreadsHistorySubAssembler( // cursors so a late callback can't move another group's cursors. newEose runs regardless. val myCursors = cursorsFor(key) return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt index bf7f2fee05..60a41ede35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt @@ -108,7 +108,7 @@ class ChatroomListNip04HistorySubAssembler( // cursors so a late callback can't move another account's cursors. newEose runs regardless. val myCursors = key.account.chatroomList.nip04History return object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessFeedFilterSubAssembler.kt index 01616b65a6..0780530666 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessFeedFilterSubAssembler.kt @@ -70,7 +70,7 @@ class ChessFeedFilterSubAssembler( newEose(key, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt index 653003ecf4..0918f1a13b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt @@ -456,7 +456,7 @@ class EventSync( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt index b36cb843e3..02b482bebf 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt @@ -668,7 +668,7 @@ class Context( val filters = relays.associateWith { listOf(responseFilter) } val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GeochatCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GeochatCommands.kt index 2547025b63..22c74904ce 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GeochatCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GeochatCommands.kt @@ -133,7 +133,7 @@ object GeochatCommands { val subId = newSubId() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NipCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NipCommand.kt index 786eefed70..21c69fabf9 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NipCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NipCommand.kt @@ -167,7 +167,7 @@ object NipCommand { val remaining = SEARCH_RELAYS.toMutableSet() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NostrConnect.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NostrConnect.kt index 33d51fe403..03629f9922 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NostrConnect.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NostrConnect.kt @@ -118,7 +118,7 @@ object NostrConnect { val subId = newSubId() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/SubscribeCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/SubscribeCommand.kt index b8afce7b90..5779bacf6c 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/SubscribeCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/SubscribeCommand.kt @@ -81,7 +81,7 @@ object SubscribeCommand { val subId = newSubId() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessRelayFetchHelper.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessRelayFetchHelper.kt index f45c499791..2a9c5a407c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessRelayFetchHelper.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessRelayFetchHelper.kt @@ -100,7 +100,7 @@ class ChessRelayFetchHelper( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt index 659ca97dcd..2260b279a5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/FeedMetadataCoordinator.kt @@ -99,7 +99,7 @@ class FeedMetadataCoordinator( val listener = if (onEvent != null) { object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -295,7 +295,7 @@ class FeedMetadataCoordinator( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -371,7 +371,7 @@ class FeedMetadataCoordinator( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt index b609d52dc2..101216858c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/PerKeyEoseManager.kt @@ -90,7 +90,7 @@ abstract class PerKeyEoseManager( newEose(queryState, relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt index fd2c6f4922..da0c3c66d9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -86,7 +86,7 @@ abstract class SingleSubEoseManager( newEose(relay, TimeUtils.now(), forFilters) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/health/RelayHealthListener.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/health/RelayHealthListener.kt index b90d9e1502..6e564e356e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/health/RelayHealthListener.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/health/RelayHealthListener.kt @@ -44,7 +44,7 @@ class RelayHealthListener( store.recordConnect(relay.url, TimeUtils.now()) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/broadcast/BroadcastTracker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/broadcast/BroadcastTracker.kt index e9f8166cc6..01f4c8a9fb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/broadcast/BroadcastTracker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/broadcast/BroadcastTracker.kt @@ -118,7 +118,7 @@ class BroadcastTracker { } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, @@ -294,7 +294,7 @@ class BroadcastTracker { } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcher.kt index 24b6401cb3..7374a54337 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcher.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcher.kt @@ -371,7 +371,7 @@ class OutboxDispatcher( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -415,7 +415,7 @@ class OutboxDispatcher( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt index e8630a810a..47a66c3f6c 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessEventBroadcaster.kt @@ -90,7 +90,7 @@ class ChessEventBroadcaster( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/WindowLoadTracker.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/WindowLoadTracker.kt index 345fe58f18..80837de516 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/WindowLoadTracker.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/relayClient/paging/WindowLoadTracker.kt @@ -212,7 +212,7 @@ fun WindowLoadTracker.trackingListener(forward: (NormalizedRelayUrl, List filter.kinds?.forEach { kind -> script[kind to relay]?.forEach { event -> - listener?.onEvent(event, isLive = false, relay = relay, forFilters = null) + kotlinx.coroutines.runBlocking { listener?.onEvent(event, isLive = false, relay = relay, forFilters = null) } } } } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcherTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcherTest.kt index 9238b5ce1d..5fe6483199 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcherTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/wot/OutboxDispatcherTest.kt @@ -171,7 +171,7 @@ class OutboxDispatcherTest { filterList.forEach { filter -> filter.kinds?.forEach { kind -> script[kind to relay]?.forEach { event -> - listener?.onEvent(event, isLive = false, relay = relay, forFilters = null) + kotlinx.coroutines.runBlocking { listener?.onEvent(event, isLive = false, relay = relay, forFilters = null) } } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index c9966e3442..3c5faabd88 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -1786,7 +1786,7 @@ fun MainContent( filters = listOf(filter), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -1845,7 +1845,7 @@ fun MainContent( relays = outbox, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index 549847e8b0..6eb0354925 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -249,7 +249,7 @@ class AccountManager internal constructor( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt index 80e88355c0..cb506f3282 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt @@ -164,7 +164,7 @@ class FollowPacksState( private fun subscribeToDiscovery() { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/MetadataPrefetch.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/MetadataPrefetch.kt index 240056ddf1..bdeb93104c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/MetadataPrefetch.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/MetadataPrefetch.kt @@ -45,7 +45,7 @@ fun RelayConnectionManager.subscribeMetadataFor( val filter = Filter(kinds = listOf(MetadataEvent.KIND), authors = pubkeys) val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FromThePackFeed.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FromThePackFeed.kt index f9b1efc816..de661f4b36 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FromThePackFeed.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FromThePackFeed.kt @@ -92,7 +92,7 @@ fun FromThePackFeed( listOf(filter), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt index ae82ca7876..3a1bc3f9b6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt @@ -102,7 +102,7 @@ fun RenderFollowPackCard( listOf(filter), listener = object : com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt index 9e07c9eb21..73057a1619 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt @@ -200,7 +200,7 @@ open class RelayConnectionManager( filters = filterMap, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -264,7 +264,7 @@ open class RelayConnectionManager( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/search/DesktopRelayUserSearchDelegate.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/search/DesktopRelayUserSearchDelegate.kt index a58d2bad74..5b74f8eccc 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/search/DesktopRelayUserSearchDelegate.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/search/DesktopRelayUserSearchDelegate.kt @@ -69,7 +69,7 @@ class DesktopRelayUserSearchDelegate( relays = relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ChessSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ChessSubscription.kt index 0d0c645669..6b6602d57f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ChessSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ChessSubscription.kt @@ -96,7 +96,7 @@ class DesktopChessSubscriptionController( relays = state.relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index ffa8f497ec..3a315d6507 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -303,7 +303,7 @@ class DesktopRelaySubscriptionsCoordinator( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -428,7 +428,7 @@ class DesktopRelaySubscriptionsCoordinator( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt index b9fa38e75b..7fe5bfb77e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt @@ -81,7 +81,7 @@ fun rememberSubscription( relays = cfg.relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ImportFollowListDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ImportFollowListDialog.kt index 52d3983dfb..41b057c510 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ImportFollowListDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ImportFollowListDialog.kt @@ -225,7 +225,7 @@ fun ImportFollowListDialog( relays = relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -277,7 +277,7 @@ fun ImportFollowListDialog( ), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 1aa9f3a1fe..66770814d0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -858,7 +858,7 @@ private suspend fun fetchMetadataForUsers( relays = relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -1666,7 +1666,7 @@ private suspend fun fetchUserLightningAddress( relays = relays, listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt index d636037723..2b18254d14 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt @@ -175,7 +175,7 @@ class ChatroomListState( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/benchmark/LaunchScenario.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/benchmark/LaunchScenario.kt index 79fb062b0e..7edddfa185 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/benchmark/LaunchScenario.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/benchmark/LaunchScenario.kt @@ -139,7 +139,7 @@ object LaunchScenario { ), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/LaunchFixtureRelayTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/LaunchFixtureRelayTest.kt index 618704a8ed..95d9c8a8d1 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/LaunchFixtureRelayTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/LaunchFixtureRelayTest.kt @@ -73,7 +73,7 @@ class LaunchFixtureRelayTest { ), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/SubscribeBeforeConnectTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/SubscribeBeforeConnectTest.kt index 2720941b42..b70fd89c9b 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/SubscribeBeforeConnectTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/testrelay/SubscribeBeforeConnectTest.kt @@ -72,7 +72,7 @@ class SubscribeBeforeConnectTest { ), listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/geode/src/main/kotlin/com/vitorpamplona/geode/mirror/MirrorWorker.kt b/geode/src/main/kotlin/com/vitorpamplona/geode/mirror/MirrorWorker.kt index c98e050e37..0557f58afa 100644 --- a/geode/src/main/kotlin/com/vitorpamplona/geode/mirror/MirrorWorker.kt +++ b/geode/src/main/kotlin/com/vitorpamplona/geode/mirror/MirrorWorker.kt @@ -686,7 +686,7 @@ class MirrorWorker( val watermark = AtomicLong(initialSince) val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/GracefulShutdownTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/GracefulShutdownTest.kt index 7a55645b7f..3d568e2550 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/GracefulShutdownTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/GracefulShutdownTest.kt @@ -125,7 +125,7 @@ class GracefulShutdownTest { val gotEose = Channel(UNLIMITED) val listener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/KtorRelayTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/KtorRelayTest.kt index 1b8f84833a..b79f81bf51 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/KtorRelayTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/KtorRelayTest.kt @@ -389,7 +389,7 @@ class KtorRelayTest { "close-test", mapOf(server.url.normalizeRelayUrl() to listOf(Filter(kinds = listOf(1)))), object : com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl, diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt index 4851956a13..7f69de4c89 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt @@ -276,7 +276,7 @@ class Nip01ComplianceTest : RelayClientTest() { "sub-A", mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -297,7 +297,7 @@ class Nip01ComplianceTest : RelayClientTest() { "sub-B", mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(4)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -385,7 +385,7 @@ class Nip01ComplianceTest : RelayClientTest() { "live-1", mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -424,7 +424,7 @@ class Nip01ComplianceTest : RelayClientTest() { "live-2", mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(1)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -465,7 +465,7 @@ class Nip01ComplianceTest : RelayClientTest() { "eph-1", mapOf(defaultRelayUrl to listOf(Filter(kinds = listOf(20_001)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -534,7 +534,7 @@ class Nip01ComplianceTest : RelayClientTest() { relayB to listOf(Filter(kinds = listOf(1))), ), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt index c4e801665a..914da8e385 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt @@ -80,7 +80,7 @@ class Nip09DeletionTest { subId, mapOf(relayUrl to listOf(filter)), object : com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip77NegentropyTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip77NegentropyTest.kt index edff7aeb69..1b156225ca 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip77NegentropyTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip77NegentropyTest.kt @@ -96,7 +96,7 @@ class Nip77NegentropyTest { compression: Boolean, ) {} - override fun onMessage(text: String) { + override suspend fun onMessage(text: String) { incoming.trySend(text) } diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/mirror/MirrorWorkerTrustOriginTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/mirror/MirrorWorkerTrustOriginTest.kt index 28d7701ebc..634331fdf9 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/mirror/MirrorWorkerTrustOriginTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/mirror/MirrorWorkerTrustOriginTest.kt @@ -99,7 +99,7 @@ class MirrorWorkerTrustOriginTest { override fun connect() { connected = true out.onOpen(0, false) - out.onMessage(frame) + kotlinx.coroutines.runBlocking { out.onMessage(frame) } } override fun disconnect() { diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/perf/LoadBenchmark.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/perf/LoadBenchmark.kt index 771a12784a..7c4acddd4b 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/perf/LoadBenchmark.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/perf/LoadBenchmark.kt @@ -258,7 +258,7 @@ class LoadBenchmark { "fanout-$i", mapOf(relayUrl to listOf(Filter(kinds = listOf(1)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -436,7 +436,7 @@ class LoadBenchmark { "fanout-$i", mapOf(relayUrl to listOf(Filter(kinds = listOf(1)))), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -568,7 +568,7 @@ class LoadBenchmark { ), ), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: com.vitorpamplona.quartz.nip01Core.core.Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/geode/src/testFixtures/kotlin/com/vitorpamplona/geode/testing/SubscriptionTesting.kt b/geode/src/testFixtures/kotlin/com/vitorpamplona/geode/testing/SubscriptionTesting.kt index d2c7ec2d04..fc50d6ef09 100644 --- a/geode/src/testFixtures/kotlin/com/vitorpamplona/geode/testing/SubscriptionTesting.kt +++ b/geode/src/testFixtures/kotlin/com/vitorpamplona/geode/testing/SubscriptionTesting.kt @@ -72,7 +72,7 @@ suspend fun NostrClient.collectUntilEoseMulti( subId, mapOf(relay to filters), object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt index 70ef709b72..4cba6e0c9f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt @@ -1489,7 +1489,7 @@ class GrapeRankCrawler( val lastEvt = AtomicLong(-1) val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, 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 8378d8140a..08e39201ad 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 @@ -329,7 +329,7 @@ class NostrClient( listeners.forEach { it.onSent(relay, cmdStr, cmd, success) } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/AdaptiveRelayLimiter.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/AdaptiveRelayLimiter.kt index 0fcb87907b..be2547ea97 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/AdaptiveRelayLimiter.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/AdaptiveRelayLimiter.kt @@ -137,7 +137,7 @@ class AdaptiveRelayLimiter( if (wait > 0) delay(wait) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt index 7c97ec3211..1964d465b5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt @@ -37,7 +37,7 @@ class EventCollector( ) { private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt index 0f19d9d75c..66e502740c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt @@ -59,7 +59,7 @@ suspend fun INostrClient.count( val listener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, @@ -117,7 +117,7 @@ suspend fun INostrClient.count( val listener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllPagesExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllPagesExt.kt index f43aed278f..f7b4834515 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllPagesExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllPagesExt.kt @@ -94,7 +94,7 @@ suspend fun INostrClient.fetchAllPages( filters: List, idleTimeoutMs: Long = 30_000L, onNewPage: ((Long) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): Int { var until: Long? = null var totalEvents = 0 @@ -172,7 +172,7 @@ suspend fun INostrClient.fetchAllPages( try { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, @@ -320,7 +320,7 @@ suspend fun INostrClient.fetchAllPages( filters: List, idleTimeoutMs: Long = 30_000L, onNewPage: ((Long) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): Int = fetchAllPages( relay = RelayUrlNormalizer.normalize(relay), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllWithHooksExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllWithHooksExt.kt index f3726f5fe2..62d13ce5e9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllWithHooksExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchAllWithHooksExt.kt @@ -102,7 +102,7 @@ suspend fun INostrClient.fetchAllWithHooks( val doneReasons = HashMap() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchFirstExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchFirstExt.kt index 2a80fcfa45..07a3f18e22 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchFirstExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientFetchFirstExt.kt @@ -96,7 +96,7 @@ suspend fun INostrClient.fetchFirst( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyFanOutExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyFanOutExt.kt index cce9d24bf1..7ac08647f5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyFanOutExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyFanOutExt.kt @@ -83,7 +83,7 @@ suspend fun negentropySyncFanOut( idleTimeoutMs: Long = 120_000L, reconcileConcurrency: Int = 2, onProgress: ((needSoFar: Int, downloaded: Int) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): NegentropyFanOutResult { require(clients.isNotEmpty()) { "at least one client is required" } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt index 89360facbf..611720876a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt @@ -163,7 +163,7 @@ suspend fun INostrClient.negentropySync( idBufferBatches: Int = maxConcurrentReqs * 4, localEntries: List = emptyList(), onProgress: ((needSoFar: Int, downloaded: Int) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): NegentropySyncResult { val need = AtomicInt(0) val windows = AtomicInt(0) @@ -242,7 +242,7 @@ suspend fun INostrClient.negentropySync( idBufferBatches: Int = maxConcurrentReqs * 4, localEntries: List = emptyList(), onProgress: ((needSoFar: Int, downloaded: Int) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): NegentropySyncResult = negentropySync( relay = RelayUrlNormalizer.normalize(relay), @@ -309,14 +309,14 @@ suspend fun INostrClient.negentropySyncOrFetch( idBufferBatches: Int = maxConcurrentReqs * 4, localEntries: List = emptyList(), onProgress: ((needSoFar: Int, downloaded: Int) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): NegentropyOrFetchResult { val seen = HashSet() var delivered = 0 // Shared dedup + cap across both phases. Returns true if the event was new and // delivered. Both phases run sequentially, so no concurrent access. - fun accept(event: Event): Boolean { + suspend fun accept(event: Event): Boolean { if ((maxEvents <= 0 || delivered < maxEvents) && seen.add(event.id)) { delivered++ onEvent(event) @@ -364,7 +364,7 @@ suspend fun INostrClient.negentropySyncOrFetch( idBufferBatches: Int = maxConcurrentReqs * 4, localEntries: List = emptyList(), onProgress: ((needSoFar: Int, downloaded: Int) -> Unit)? = null, - onEvent: (Event) -> Unit, + onEvent: suspend (Event) -> Unit, ): NegentropyOrFetchResult = negentropySyncOrFetch( relay = RelayUrlNormalizer.normalize(relay), @@ -876,7 +876,7 @@ private suspend fun INostrClient.reconcileStreaming( if (relay.url == targetUrl) clock.bump() } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, @@ -1103,7 +1103,7 @@ internal suspend fun INostrClient.fetchByIds( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, 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 c1aa8c14a2..fc725dbe84 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 @@ -132,7 +132,7 @@ suspend fun INostrClient.publishAndCollectResults( } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt index 5014eada6f..6b536de30b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt @@ -37,7 +37,7 @@ class RelayInsertConfirmationCollector( ) { private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt index 78b304edae..86313d9a48 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt @@ -50,7 +50,7 @@ class RelayLogger( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt index 65f2c6aae8..1853aec5e3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt @@ -40,7 +40,7 @@ class RelayNotifier( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt index 1c5bc662de..b45e652421 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt @@ -102,7 +102,7 @@ class RelayAuthenticator( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/counts/RelayActiveCountStates.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/counts/RelayActiveCountStates.kt index dba6888dc7..d03077ba2e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/counts/RelayActiveCountStates.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/counts/RelayActiveCountStates.kt @@ -45,7 +45,7 @@ class RelayActiveCountStates( queryStates.put(relay.url, CountQueryState()) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt index a8ffa37d63..65f61e8057 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTracker.kt @@ -76,7 +76,7 @@ class RelayLimitsTracker( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectConnectionListener.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectConnectionListener.kt index 5d7fba5ffc..75e6ce6363 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectConnectionListener.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectConnectionListener.kt @@ -48,7 +48,7 @@ open class RedirectConnectionListener( listener.onSent(relay, cmdStr, cmd, success) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RelayConnectionListener.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RelayConnectionListener.kt index feefbb532f..8f70a9cc11 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RelayConnectionListener.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RelayConnectionListener.kt @@ -53,7 +53,7 @@ interface RelayConnectionListener { /** * New error */ - fun onIncomingMessage( + suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt index 2d9a44ea31..97c48dc5fa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolCounts.kt @@ -123,7 +123,7 @@ class PoolCounts { } } - fun onIncomingMessage( + suspend fun onIncomingMessage( relay: IRelayClient, msg: Message, ) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt index c249d2aa3e..928cfd0bc2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt @@ -178,7 +178,7 @@ class PoolEventOutbox { } } - fun onIncomingMessage( + suspend fun onIncomingMessage( relay: NormalizedRelayUrl, msg: Message, ) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt index 7d4b08315b..7c87d14712 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequests.kt @@ -238,7 +238,7 @@ class PoolRequests( /** * When a new message is received by the relay, updates the sub */ - fun onIncomingMessage( + suspend fun onIncomingMessage( relay: IRelayClient, msg: Message, ) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 22c2e0003b..62fe531d86 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -248,7 +248,7 @@ class RelayPool( listener.onDisconnected(relay) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/DynamicSubscription.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/DynamicSubscription.kt index 1821f4ad86..f6eeb770dd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/DynamicSubscription.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/DynamicSubscription.kt @@ -34,7 +34,7 @@ class DynamicSubscription( SubscriptionHandle { val subId = RandomInstance.randomChars(10) - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientFetchAsFlowExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientFetchAsFlowExt.kt index 1cda8b7e7c..6dd2ccaae5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientFetchAsFlowExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientFetchAsFlowExt.kt @@ -63,7 +63,7 @@ fun INostrClient.fetchAsFlow( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientSubscribeAsFlowExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientSubscribeAsFlowExt.kt index 50b6af68ab..4d7736ac71 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientSubscribeAsFlowExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/NostrClientSubscribeAsFlowExt.kt @@ -69,7 +69,7 @@ fun INostrClient.subscribeAsFlow( val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt index a51d2f1df2..f75dfbe365 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/RelayActiveRequestStates.kt @@ -46,7 +46,7 @@ class RelayActiveRequestStates( subStates[relay.url] = RequestSubscriptionState() } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/StaticSubscription.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/StaticSubscription.kt index 1e87560adb..d33e9ff080 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/StaticSubscription.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/StaticSubscription.kt @@ -35,7 +35,7 @@ class StaticSubscription( SubscriptionHandle { val subId = RandomInstance.randomChars(10) - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/SubscriptionListener.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/SubscriptionListener.kt index 45f614237d..15486f55af 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/SubscriptionListener.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/SubscriptionListener.kt @@ -30,7 +30,7 @@ interface SubscriptionListener { forFilters: List?, ) {} - fun onEvent( + suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/stats/RelayReqStats.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/stats/RelayReqStats.kt index 2da8b3ca6d..0dcc1291df 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/stats/RelayReqStats.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/reqs/stats/RelayReqStats.kt @@ -37,7 +37,7 @@ class RelayReqStats( private val clientListener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 5005e63c9c..d0f1b7bc16 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -156,7 +156,7 @@ open class BasicRelayClient( listener.onConnected(this@BasicRelayClient, pingMillis, compression) } - override fun onMessage(text: String) { + override suspend fun onMessage(text: String) { try { val msg = decoder.decode(text) listener.onIncomingMessage(this@BasicRelayClient, text, msg) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/standalone/StandaloneRelayClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/standalone/StandaloneRelayClient.kt index 5063f03319..52f7947b2a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/standalone/StandaloneRelayClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/standalone/StandaloneRelayClient.kt @@ -66,7 +66,7 @@ class StandaloneRelayClient( syncFilters() } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt index b6548e1a2c..73b41bd000 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt @@ -83,7 +83,7 @@ class RelayStats( get(relay.url).addBytesSent(cmdStr.bytesUsedInMemory()) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt index fef0f36a6b..4f4cdc522b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt @@ -30,7 +30,7 @@ interface WebSocketListener { compression: Boolean, ) - fun onMessage(text: String) + suspend fun onMessage(text: String) fun onClosed( code: Int, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerService.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerService.kt index 4be37a0b84..bc473bcaf1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerService.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerService.kt @@ -183,7 +183,7 @@ class NostrConnectSignerService( val subId = newSubId() val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserver.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserver.kt index 1e293e3d05..d9d40ec0f5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserver.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserver.kt @@ -183,7 +183,7 @@ class RelayObserver : RelayConnectionListener { } } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProber.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProber.kt index 48d33d9185..711c38551a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProber.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProber.kt @@ -254,7 +254,7 @@ class RelayProber( val subId = newSubId() val subListener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyManager.kt index 3984c1436d..83ad948534 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip77Negentropy/NegentropyManager.kt @@ -94,7 +94,7 @@ class NegentropyManager( relay.sendIfConnected(session.close()) } - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt index 9e33fb9b91..504d0ff9aa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleMeshManager.kt @@ -260,7 +260,7 @@ class BleMeshManager( private inner class ClientConnectionListener( val peerUuid: String, ) : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt index 1a0771d2e7..2f71344a25 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBEBle/relay/BleNostrClient.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nipBEBle.relay import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nipBEBle.BleConfig @@ -31,8 +32,14 @@ import com.vitorpamplona.quartz.nipBEBle.protocol.BleChunkAssembler import com.vitorpamplona.quartz.nipBEBle.protocol.BleMessageChunker import com.vitorpamplona.quartz.nipBEBle.transport.BleTransport import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi @@ -72,6 +79,31 @@ class BleNostrClient( private val sendQueue = Channel>(Channel.UNLIMITED) private val isSending = AtomicBoolean(false) + /** Parsed messages waiting to reach the suspending listener — see [onChunkReceived]. */ + private val incoming = Channel>(Channel.UNLIMITED) + + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + private val pump = + scope.launch { + for ((raw, msg) in incoming) { + try { + listener.onIncomingMessage(this@BleNostrClient, raw, msg) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + // One peer's bad message must not end the pump for the rest. + Log.e("BleNostrClient", "Failure handling message from ${peer.deviceUuid}: $raw", e) + } + } + } + + /** Stops the [pump]; the client is unusable afterwards, like a closed socket. */ + fun release() { + incoming.close() + scope.cancel() + } + override fun isConnected(): Boolean = connected override fun needsToReconnect(): Boolean = !connected @@ -144,7 +176,13 @@ class BleNostrClient( try { val msg = OptimizedJsonMapper.fromJsonToMessage(message) - listener.onIncomingMessage(this, message, msg) + // The platform hands BLE notifications to a callback that cannot + // suspend, and the listener chain now does — so the message is + // handed off rather than delivered here. Same shape the websocket + // transport already uses: UNLIMITED so this callback never blocks + // the BLE stack, drained by ONE coroutine so message order survives + // the boundary. + incoming.trySend(message to msg) } catch (e: Exception) { Log.e("BleNostrClient", "Failed to parse message from ${peer.deviceUuid}: $message", e) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt index 6547aeb9bb..39dae9ad12 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/limits/RelayLimitsTrackerTest.kt @@ -70,67 +70,72 @@ class RelayLimitsTrackerTest { } @Test - fun cachesLimitsPerRelay() { - val (limits, listener) = setup() - val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) + fun cachesLimitsPerRelay() = + kotlinx.coroutines.test.runTest { + val (limits, listener) = setup() + val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) - assertNull(limits.get(relay.url), "No limits before any LIMITS message") + assertNull(limits.get(relay.url), "No limits before any LIMITS message") - listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = true, maxLimit = 200)) + listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = true, maxLimit = 200)) - assertEquals(true, limits.get(relay.url)?.canWrite) - assertEquals(200, limits.get(relay.url)?.maxLimit) - assertEquals(limits.get(relay.url), limits.limitsFlow.value[relay.url]) - } + assertEquals(true, limits.get(relay.url)?.canWrite) + assertEquals(200, limits.get(relay.url)?.maxLimit) + assertEquals(limits.get(relay.url), limits.limitsFlow.value[relay.url]) + } @Test - fun laterLimitsReplaceEarlierOnes() { - val (limits, listener) = setup() - val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) + fun laterLimitsReplaceEarlierOnes() = + kotlinx.coroutines.test.runTest { + val (limits, listener) = setup() + val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) - listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = false, maxLimit = 200)) - listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = true, maxLimit = 500)) + listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = false, maxLimit = 200)) + listener.onIncomingMessage(relay, "", LimitsMessage(canWrite = true, maxLimit = 500)) - // A relay re-advertises LIMITS when rights change (e.g. after AUTH flips can_write). - assertEquals(true, limits.get(relay.url)?.canWrite) - assertEquals(500, limits.get(relay.url)?.maxLimit) - } + // A relay re-advertises LIMITS when rights change (e.g. after AUTH flips can_write). + assertEquals(true, limits.get(relay.url)?.canWrite) + assertEquals(500, limits.get(relay.url)?.maxLimit) + } @Test - fun tracksLimitsForDistinctRelaysIndependently() { - val (limits, listener) = setup() - val relayA = FakeRelayClient(NormalizedRelayUrl("wss://a.example/")) - val relayB = FakeRelayClient(NormalizedRelayUrl("wss://b.example/")) + fun tracksLimitsForDistinctRelaysIndependently() = + kotlinx.coroutines.test.runTest { + val (limits, listener) = setup() + val relayA = FakeRelayClient(NormalizedRelayUrl("wss://a.example/")) + val relayB = FakeRelayClient(NormalizedRelayUrl("wss://b.example/")) - listener.onIncomingMessage(relayA, "", LimitsMessage(maxLimit = 100)) - listener.onIncomingMessage(relayB, "", LimitsMessage(maxLimit = 999)) + listener.onIncomingMessage(relayA, "", LimitsMessage(maxLimit = 100)) + listener.onIncomingMessage(relayB, "", LimitsMessage(maxLimit = 999)) - assertEquals(100, limits.get(relayA.url)?.maxLimit) - assertEquals(999, limits.get(relayB.url)?.maxLimit) - assertEquals(2, limits.snapshot().size) - } + assertEquals(100, limits.get(relayA.url)?.maxLimit) + assertEquals(999, limits.get(relayB.url)?.maxLimit) + assertEquals(2, limits.snapshot().size) + } @Test - fun dropsCachedLimitsOnDisconnect() { - val (limits, listener) = setup() - val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) + fun dropsCachedLimitsOnDisconnect() = + kotlinx.coroutines.test.runTest { + val (limits, listener) = setup() + val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) - listener.onIncomingMessage(relay, "", LimitsMessage(canRead = true)) - assertTrue(limits.get(relay.url) != null) + listener.onIncomingMessage(relay, "", LimitsMessage(canRead = true)) + assertTrue(limits.get(relay.url) != null) - listener.onDisconnected(relay) - assertNull(limits.get(relay.url), "Limits are connection-scoped and cleared on disconnect") - assertTrue(limits.snapshot().isEmpty()) - } + listener.onDisconnected(relay) + assertNull(limits.get(relay.url), "Limits are connection-scoped and cleared on disconnect") + assertTrue(limits.snapshot().isEmpty()) + } @Test - fun ignoresNonLimitsMessages() { - val (limits, listener) = setup() - val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) + fun ignoresNonLimitsMessages() = + kotlinx.coroutines.test.runTest { + val (limits, listener) = setup() + val relay = FakeRelayClient(NormalizedRelayUrl("wss://relay.example/")) - listener.onIncomingMessage(relay, "", EoseMessage("sub1")) + listener.onIncomingMessage(relay, "", EoseMessage("sub1")) - assertNull(limits.get(relay.url)) - assertTrue(limits.snapshot().isEmpty()) - } + assertNull(limits.get(relay.url)) + assertTrue(limits.snapshot().isEmpty()) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxAuthTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxAuthTest.kt index f8250eb9a1..8d0fa9b9df 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxAuthTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxAuthTest.kt @@ -62,13 +62,13 @@ class PoolEventOutboxAuthTest { relays.forEach { onSent(it, EventCmd(event)) } } - private fun PoolEventOutbox.nak( + private suspend fun PoolEventOutbox.nak( event: Event, relay: NormalizedRelayUrl, message: String, ) = onIncomingMessage(relay, OkMessage(event.id, false, message)) - private fun PoolEventOutbox.ok( + private suspend fun PoolEventOutbox.ok( event: Event, relay: NormalizedRelayUrl, ) = onIncomingMessage(relay, OkMessage(event.id, true, "")) @@ -104,67 +104,71 @@ class PoolEventOutboxAuthTest { } @Test - fun authRequiredResetsTheTriesBudgetAcrossManyResends() { - val outbox = PoolEventOutbox() - val ev = event("ff".repeat(32)) + fun authRequiredResetsTheTriesBudgetAcrossManyResends() = + kotlinx.coroutines.test.runTest { + val outbox = PoolEventOutbox() + val ev = event("ff".repeat(32)) - outbox.publish(ev, setOf(relay)) // first try + outbox.publish(ev, setOf(relay)) // first try - // A flapping relay / slow AUTH handshake re-pumps the still-pending event many more times - // than the 4-try cap, each NAK'd auth-required. newTry() (the send path) grows `tries` and - // is not auth-aware, so unless auth-required resets the retry budget these sends would trip - // Tries.isDone() and drop the event (with a spurious give-up) before AUTH ever lands. - repeat(8) { i -> - assertNull(outbox.onSent(relay, EventCmd(ev)), "must not give up on re-pump $i") - outbox.nak(ev, relay, "auth-required: authenticate first") + // A flapping relay / slow AUTH handshake re-pumps the still-pending event many more times + // than the 4-try cap, each NAK'd auth-required. newTry() (the send path) grows `tries` and + // is not auth-aware, so unless auth-required resets the retry budget these sends would trip + // Tries.isDone() and drop the event (with a spurious give-up) before AUTH ever lands. + repeat(8) { i -> + assertNull(outbox.onSent(relay, EventCmd(ev)), "must not give up on re-pump $i") + outbox.nak(ev, relay, "auth-required: authenticate first") + } + assertEquals(setOf(relay), outbox.pendingRelaysFor(ev.id)) + + // AUTH finally completes -> the event delivers. + outbox.ok(ev, relay) + assertNull(outbox.pendingRelaysFor(ev.id)) } - assertEquals(setOf(relay), outbox.pendingRelaysFor(ev.id)) - - // AUTH finally completes -> the event delivers. - outbox.ok(ev, relay) - assertNull(outbox.pendingRelaysFor(ev.id)) - } @Test - fun terminalRejectionStillDiscardsImmediately() { - val outbox = PoolEventOutbox() - val ev = event("cc".repeat(32)) + fun terminalRejectionStillDiscardsImmediately() = + kotlinx.coroutines.test.runTest { + val outbox = PoolEventOutbox() + val ev = event("cc".repeat(32)) - outbox.publish(ev, setOf(relay)) - outbox.nak(ev, relay, "invalid: bad signature") + outbox.publish(ev, setOf(relay)) + outbox.nak(ev, relay, "invalid: bad signature") - assertNull(outbox.pendingRelaysFor(ev.id)) - } + assertNull(outbox.pendingRelaysFor(ev.id)) + } @Test - fun givesUpAndSignalsAfterExhaustingTryBudget() { - val outbox = PoolEventOutbox() - val ev = event("ee".repeat(32)) + fun givesUpAndSignalsAfterExhaustingTryBudget() = + kotlinx.coroutines.test.runTest { + val outbox = PoolEventOutbox() + val ev = event("ee".repeat(32)) - // markAsSending + first onSent (1 try). Tries budget is >3 tries. - outbox.publish(ev, setOf(relay)) - // attempts 2, 3 stay under budget and signal nothing. - assertNull(outbox.onSent(relay, EventCmd(ev))) - assertNull(outbox.onSent(relay, EventCmd(ev))) - // the 4th attempt exhausts the budget -> event is returned (gave up) and dropped. - assertEquals(ev.id, outbox.onSent(relay, EventCmd(ev))?.id) - assertNull(outbox.pendingRelaysFor(ev.id)) - } + // markAsSending + first onSent (1 try). Tries budget is >3 tries. + outbox.publish(ev, setOf(relay)) + // attempts 2, 3 stay under budget and signal nothing. + assertNull(outbox.onSent(relay, EventCmd(ev))) + assertNull(outbox.onSent(relay, EventCmd(ev))) + // the 4th attempt exhausts the budget -> event is returned (gave up) and dropped. + assertEquals(ev.id, outbox.onSent(relay, EventCmd(ev))?.id) + assertNull(outbox.pendingRelaysFor(ev.id)) + } @Test - fun ordinaryTransientFailureStillBounded() { - val outbox = PoolEventOutbox() - val ev = event("dd".repeat(32)) + fun ordinaryTransientFailureStillBounded() = + kotlinx.coroutines.test.runTest { + val outbox = PoolEventOutbox() + val ev = event("dd".repeat(32)) - outbox.publish(ev, setOf(relay)) - // 3 non-auth error responses exhaust the retry budget. Responses only - // accumulate here; the drop happens on the next send attempt. - repeat(3) { outbox.nak(ev, relay, "error: rate-limited") } - assertEquals(setOf(relay), outbox.pendingRelaysFor(ev.id)) + outbox.publish(ev, setOf(relay)) + // 3 non-auth error responses exhaust the retry budget. Responses only + // accumulate here; the drop happens on the next send attempt. + repeat(3) { outbox.nak(ev, relay, "error: rate-limited") } + assertEquals(setOf(relay), outbox.pendingRelaysFor(ev.id)) - // The next resend attempt observes the exhausted budget and drops the event - // (unlike auth-required, which never poisons the budget). - outbox.onSent(relay, EventCmd(ev)) - assertNull(outbox.pendingRelaysFor(ev.id)) - } + // The next resend attempt observes the exhausted budget and drops the event + // (unlike auth-required, which never poisons the budget). + outbox.onSent(relay, EventCmd(ev)) + assertNull(outbox.pendingRelaysFor(ev.id)) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsRefusalTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsRefusalTest.kt index 2e26da853a..91f5c03c4e 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsRefusalTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolRequestsRefusalTest.kt @@ -75,123 +75,129 @@ class PoolRequestsRefusalTest { return sent } - private fun close( + private suspend fun close( pool: PoolRequests, subId: String, reason: String, ) = pool.onIncomingMessage(FakeRelayClient(relay), ClosedMessage(subId, reason)) @Test - fun stopsReplayingAThriceRefusedFilterAcrossReconnects() { - val pool = PoolRequests(maxRefusalsBeforeSuppress = 3) - pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) + fun stopsReplayingAThriceRefusedFilterAcrossReconnects() = + kotlinx.coroutines.test.runTest { + val pool = PoolRequests(maxRefusalsBeforeSuppress = 3) + pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) - // Under the threshold, each reconnect still replays the REQ (giving the relay a chance). - repeat(3) { attempt -> - val sent = reconnectAndSync(pool) - assertEquals(1, sent.filterIsInstance().size, "reconnect #$attempt should replay the REQ") - close(pool, "sub", "unsupported: too many filters") + // Under the threshold, each reconnect still replays the REQ (giving the relay a chance). + repeat(3) { attempt -> + val sent = reconnectAndSync(pool) + assertEquals(1, sent.filterIsInstance().size, "reconnect #$attempt should replay the REQ") + close(pool, "sub", "unsupported: too many filters") + } + + // Once the same filter has been refused [maxRefusalsBeforeSuppress] times, stop replaying it. + val suppressed = reconnectAndSync(pool) + assertTrue(suppressed.filterIsInstance().isEmpty(), "a thrice-refused filter must not be replayed again") } - // Once the same filter has been refused [maxRefusalsBeforeSuppress] times, stop replaying it. - val suppressed = reconnectAndSync(pool) - assertTrue(suppressed.filterIsInstance().isEmpty(), "a thrice-refused filter must not be replayed again") - } - @Test - fun aMeaningfulFilterChangeReEnablesTheReq() { - val pool = PoolRequests(maxRefusalsBeforeSuppress = 2) - pool.addOrUpdate("sub", mapOf(relay to plainFilter(1)), null) + fun aMeaningfulFilterChangeReEnablesTheReq() = + kotlinx.coroutines.test.runTest { + val pool = PoolRequests(maxRefusalsBeforeSuppress = 2) + pool.addOrUpdate("sub", mapOf(relay to plainFilter(1)), null) - repeat(2) { - reconnectAndSync(pool) - close(pool, "sub", "unsupported: too many filters") - } - assertTrue(reconnectAndSync(pool).filterIsInstance().isEmpty(), "refused filter is suppressed") + repeat(2) { + reconnectAndSync(pool) + close(pool, "sub", "unsupported: too many filters") + } + assertTrue(reconnectAndSync(pool).filterIsInstance().isEmpty(), "refused filter is suppressed") - // The app changes the subscription's filter (different kind) — the relay may now accept it. - pool.addOrUpdate("sub", mapOf(relay to plainFilter(30023)), null) - assertEquals(1, reconnectAndSync(pool).filterIsInstance().size, "a changed filter must be tried again") - } - - @Test - fun aSearchOnlyRelayStopsReceivingPlainReqsFromEveryNewSubOnOneConnection() { - // The search.nos.today case: 6 different subscriptions, one connection, each a - // distinct plain feed filter the relay CLOSES with `error: search filter is required`. - // Per-filter memory can't help (the filters differ); the relay-wide block must. - val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) - - val sent = mutableListOf>() // subId -> did a REQ go out - - fun mountSub( - subId: String, - filter: List, - ) { - val affected = pool.addOrUpdate(subId, mapOf(relay to filter), null) - var reqSent = false - pool.sendToRelayIfChanged(subId, affected) { _, cmd -> if (cmd is ReqCmd) reqSent = true } - sent.add(subId to reqSent) - close(pool, subId, "error: search filter is required") + // The app changes the subscription's filter (different kind) — the relay may now accept it. + pool.addOrUpdate("sub", mapOf(relay to plainFilter(30023)), null) + assertEquals(1, reconnectAndSync(pool).filterIsInstance().size, "a changed filter must be tried again") } - mountSub("sub1", plainFilter(1)) - mountSub("sub2", plainFilter(2)) - mountSub("sub3", plainFilter(3)) - mountSub("sub4", plainFilter(4)) - - assertTrue(sent[0].second && sent[1].second, "the first two plain subs are sent (learning the relay is search-only)") - assertTrue(!sent[2].second && !sent[3].second, "after two refusals, further plain subs are not sent to a search-only relay") - } - @Test - fun aCapabilityBlockedRelayIsDroppedFromDesiredRelays() { - // The socket-closing half: once a relay is capability-blocked and no desired sub can - // use it, it leaves the desired-relay set so the pool disconnects it. - val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) - pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) - assertTrue(relay in pool.desiredRelays.value, "the relay is wanted before it refuses anything") + fun aSearchOnlyRelayStopsReceivingPlainReqsFromEveryNewSubOnOneConnection() = + kotlinx.coroutines.test.runTest { + // The search.nos.today case: 6 different subscriptions, one connection, each a + // distinct plain feed filter the relay CLOSES with `error: search filter is required`. + // Per-filter memory can't help (the filters differ); the relay-wide block must. + val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) - close(pool, "sub", "error: search filter is required") - assertTrue(relay in pool.desiredRelays.value, "one refusal doesn't drop it yet") + val sent = mutableListOf>() // subId -> did a REQ go out - close(pool, "sub", "error: search filter is required") - assertTrue(relay !in pool.desiredRelays.value, "a search-only relay with only plain subs is dropped (socket closes)") - } + suspend fun mountSub( + subId: String, + filter: List, + ) { + val affected = pool.addOrUpdate(subId, mapOf(relay to filter), null) + var reqSent = false + pool.sendToRelayIfChanged(subId, affected) { _, cmd -> if (cmd is ReqCmd) reqSent = true } + sent.add(subId to reqSent) + close(pool, subId, "error: search filter is required") + } - @Test - fun aSearchOnlyRelayStaysWantedWhileASearchSubNeedsIt() { - val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) - pool.addOrUpdate("plain", mapOf(relay to plainFilter()), null) - pool.addOrUpdate("search", mapOf(relay to listOf(Filter(kinds = listOf(1), search = "nostr"))), null) + mountSub("sub1", plainFilter(1)) + mountSub("sub2", plainFilter(2)) + mountSub("sub3", plainFilter(3)) + mountSub("sub4", plainFilter(4)) - close(pool, "plain", "error: search filter is required") - close(pool, "plain", "error: search filter is required") - - assertTrue(relay in pool.desiredRelays.value, "the relay stays wanted: a search sub still has a usable filter for it") - } - - @Test - fun authRequiredAndRateLimitedAreNeverSuppressed() { - val pool = PoolRequests(maxRefusalsBeforeSuppress = 2) - pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) - - repeat(4) { - reconnectAndSync(pool) - close(pool, "sub", MachineReadablePrefix.AUTH_REQUIRED.format("authenticate first")) + assertTrue(sent[0].second && sent[1].second, "the first two plain subs are sent (learning the relay is search-only)") + assertTrue(!sent[2].second && !sent[3].second, "after two refusals, further plain subs are not sent to a search-only relay") } - assertEquals(1, reconnectAndSync(pool).filterIsInstance().size, "auth-required must keep replaying (auth resolves it)") - val pool2 = PoolRequests(maxRefusalsBeforeSuppress = 2) - pool2.addOrUpdate("sub", mapOf(relay to plainFilter()), null) - repeat(4) { + @Test + fun aCapabilityBlockedRelayIsDroppedFromDesiredRelays() = + kotlinx.coroutines.test.runTest { + // The socket-closing half: once a relay is capability-blocked and no desired sub can + // use it, it leaves the desired-relay set so the pool disconnects it. + val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) + pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) + assertTrue(relay in pool.desiredRelays.value, "the relay is wanted before it refuses anything") + + close(pool, "sub", "error: search filter is required") + assertTrue(relay in pool.desiredRelays.value, "one refusal doesn't drop it yet") + + close(pool, "sub", "error: search filter is required") + assertTrue(relay !in pool.desiredRelays.value, "a search-only relay with only plain subs is dropped (socket closes)") + } + + @Test + fun aSearchOnlyRelayStaysWantedWhileASearchSubNeedsIt() = + kotlinx.coroutines.test.runTest { + val pool = PoolRequests(relayRefusals = RelayReqRefusals(threshold = 2)) + pool.addOrUpdate("plain", mapOf(relay to plainFilter()), null) + pool.addOrUpdate("search", mapOf(relay to listOf(Filter(kinds = listOf(1), search = "nostr"))), null) + + close(pool, "plain", "error: search filter is required") + close(pool, "plain", "error: search filter is required") + + assertTrue(relay in pool.desiredRelays.value, "the relay stays wanted: a search sub still has a usable filter for it") + } + + @Test + fun authRequiredAndRateLimitedAreNeverSuppressed() = + kotlinx.coroutines.test.runTest { + val pool = PoolRequests(maxRefusalsBeforeSuppress = 2) + pool.addOrUpdate("sub", mapOf(relay to plainFilter()), null) + + repeat(4) { + reconnectAndSync(pool) + close(pool, "sub", MachineReadablePrefix.AUTH_REQUIRED.format("authenticate first")) + } + assertEquals(1, reconnectAndSync(pool).filterIsInstance().size, "auth-required must keep replaying (auth resolves it)") + + val pool2 = PoolRequests(maxRefusalsBeforeSuppress = 2) + pool2.addOrUpdate("sub", mapOf(relay to plainFilter()), null) + repeat(4) { + pool2.onConnecting(relay) + val sent = mutableListOf() + pool2.syncState(relay) { sent.add(it) } + pool2.onIncomingMessage(FakeRelayClient(relay), ClosedMessage("sub", MachineReadablePrefix.RATE_LIMITED.format("slow down"))) + } pool2.onConnecting(relay) val sent = mutableListOf() pool2.syncState(relay) { sent.add(it) } - pool2.onIncomingMessage(FakeRelayClient(relay), ClosedMessage("sub", MachineReadablePrefix.RATE_LIMITED.format("slow down"))) + assertEquals(1, sent.filterIsInstance().size, "rate-limited must keep replaying (the limiter spaces it out)") } - pool2.onConnecting(relay) - val sent = mutableListOf() - pool2.syncState(relay) { sent.add(it) } - assertEquals(1, sent.filterIsInstance().size, "rate-limited must keep replaying (the limiter spaces it out)") - } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocketTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocketTest.kt index 2b9cda14cd..4874586ebd 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocketTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocketTest.kt @@ -74,7 +74,7 @@ class InProcessWebSocketTest { callbacks.trySend("open") } - override fun onMessage(text: String) { + override suspend fun onMessage(text: String) { callbacks.trySend("message") } @@ -132,7 +132,7 @@ class InProcessWebSocketTest { ) { } - override fun onMessage(text: String) { + override suspend fun onMessage(text: String) { // Answer the AUTH challenge immediately, the way // RelayAuthenticator does. The socket must be fully // wired by the time any server frame is delivered, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt index 12320fcfc5..3dcf86afd1 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt @@ -141,7 +141,7 @@ class NostrConnectSignerServiceTest { published.add(event) } - fun deliver(event: Event) { + suspend fun deliver(event: Event) { listener?.onEvent(event, isLive = true, relay = RelayUrlNormalizer.normalizeOrNull("wss://relay.example.com")!!, forFilters = null) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserverTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserverTest.kt index 6522dd5c64..efa6af497a 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserverTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayObserverTest.kt @@ -73,256 +73,276 @@ class RelayObserverTest { // ---- what we measured --------------------------------------------------- @Test - fun `rtt-open is the transport handshake rather than our own queueing`() { - // pingMillis is receivedResponseAtMillis - sentRequestAtMillis: it starts - // when the upgrade request goes out, so it excludes time the call spent - // queued in the client's dispatcher. Timing the enqueue instead published - // our own backlog as the relay's latency — a median of 33.5 SECONDS on a - // 16,507-relay fan-out, against a true minimum of 140ms — into the field - // aggregators rank relays by. - val o = RelayObserver() - o.onConnected(client(url), 140, false) - assertEquals(140L, o.only().rttOpenMs) - } + fun `rtt-open is the transport handshake rather than our own queueing`() = + kotlinx.coroutines.test.runTest { + // pingMillis is receivedResponseAtMillis - sentRequestAtMillis: it starts + // when the upgrade request goes out, so it excludes time the call spent + // queued in the client's dispatcher. Timing the enqueue instead published + // our own backlog as the relay's latency — a median of 33.5 SECONDS on a + // 16,507-relay fan-out, against a true minimum of 140ms — into the field + // aggregators rank relays by. + val o = RelayObserver() + o.onConnected(client(url), 140, false) + assertEquals(140L, o.only().rttOpenMs) + } @Test - fun `a handshake the transport could not time publishes no time`() { - val o = RelayObserver() - o.onConnected(client(url), 0, false) + fun `a handshake the transport could not time publishes no time`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnected(client(url), 0, false) - val obs = o.only() - assertTrue(obs.reachable, "it opened, and that much is known") - assertNull(obs.rttOpenMs, "unmeasurable is not zero") - } + val obs = o.only() + assertTrue(obs.reachable, "it opened, and that much is known") + assertNull(obs.rttOpenMs, "unmeasurable is not zero") + } @Test - fun `an opened connection is timed rather than assumed`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) + fun `an opened connection is timed rather than assumed`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) - val obs = o.only() - assertTrue(obs.reachable) - assertNotNull(obs.rttOpenMs, "rtt-open must be measured — aggregators rank on it") - assertNull(obs.error) - } + val obs = o.only() + assertTrue(obs.reachable) + assertNotNull(obs.rttOpenMs, "rtt-open must be measured — aggregators rank on it") + assertNull(obs.error) + } @Test - fun `the read clock runs from the first REQ to the first EOSE`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.onSent(client(url), "", ReqCmd("sub", emptyList()), true) - o.onIncomingMessage(client(url), "", EoseMessage("sub")) + fun `the read clock runs from the first REQ to the first EOSE`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.onSent(client(url), "", ReqCmd("sub", emptyList()), true) + o.onIncomingMessage(client(url), "", EoseMessage("sub")) - assertNotNull(o.only().rttReadMs) - } + assertNotNull(o.only().rttReadMs) + } @Test - fun `the write clock runs from the first EVENT to its OK`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.onIncomingMessage(client(url), "", OkMessage("id", true, "")) + fun `the write clock runs from the first EVENT to its OK`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.onIncomingMessage(client(url), "", OkMessage("id", true, "")) - assertNull(o.collectUnreported().single().rttWriteMs, "an OK with nothing sent behind it times nothing") - } + assertNull(o.collectUnreported().single().rttWriteMs, "an OK with nothing sent behind it times nothing") + } @Test - fun `a non-REQ command does not start the read clock`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.onSent(client(url), "", CloseCmd("sub"), true) - o.onIncomingMessage(client(url), "", EoseMessage("sub")) + fun `a non-REQ command does not start the read clock`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.onSent(client(url), "", CloseCmd("sub"), true) + o.onIncomingMessage(client(url), "", EoseMessage("sub")) - assertNull(o.only().rttReadMs) - } + assertNull(o.only().rttReadMs) + } // ---- what we refuse to claim -------------------------------------------- @Test - fun `a connection that never opened records the reason and no latency`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onCannotConnect(client(url), "Expected HTTP 101 response but was '503 Service Unavailable'") + fun `a connection that never opened records the reason and no latency`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onCannotConnect(client(url), "Expected HTTP 101 response but was '503 Service Unavailable'") - val obs = o.only() - assertFalse(obs.reachable) - assertNull(obs.rttOpenMs, "nothing opened, so there is nothing to time") - assertTrue(obs.error!!.contains("503")) - } + val obs = o.only() + assertFalse(obs.reachable) + assertNull(obs.rttOpenMs, "nothing opened, so there is nothing to time") + assertTrue(obs.error!!.contains("503")) + } @Test - fun `a relay that answered stays answered through a later failure`() { - // A relay that worked a minute ago and blipped now is not the same thing - // as one that never answered, and only the writer decides which record - // that becomes. A single failure must not erase the success under it. - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.onCannotConnect(client(url), "connection reset") + fun `a relay that answered stays answered through a later failure`() = + kotlinx.coroutines.test.runTest { + // A relay that worked a minute ago and blipped now is not the same thing + // as one that never answered, and only the writer decides which record + // that becomes. A single failure must not erase the success under it. + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.onCannotConnect(client(url), "connection reset") - assertTrue(o.only().reachable, "one bad minute must not bury a relay that answered") - } + assertTrue(o.only().reachable, "one bad minute must not bury a relay that answered") + } @Test - fun `a reconnect clears the previous attempt's error`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onCannotConnect(client(url), "timeout") - o.onConnecting(client(url)) + fun `a reconnect clears the previous attempt's error`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onCannotConnect(client(url), "timeout") + o.onConnecting(client(url)) - assertNull(o.only().error, "a stale error would report a live relay as broken forever") - } + assertNull(o.only().error, "a stale error would report a live relay as broken forever") + } // ---- AUTH, which is why an anonymous crawl finds a relay empty ------------ @Test - fun `a demand for AUTH is recorded from either shape`() { - val challenged = RelayObserver() - challenged.onIncomingMessage(client(url), "", AuthMessage("challenge")) - assertTrue(challenged.only().authRequired) + fun `a demand for AUTH is recorded from either shape`() = + kotlinx.coroutines.test.runTest { + val challenged = RelayObserver() + challenged.onIncomingMessage(client(url), "", AuthMessage("challenge")) + assertTrue(challenged.only().authRequired) - val closed = RelayObserver() - closed.onIncomingMessage(client(url), "", ClosedMessage("sub", "auth-required: subscribers only")) - val obs = closed.only() - assertTrue(obs.authRequired) - assertEquals("auth-required", obs.closedReason) - } + val closed = RelayObserver() + closed.onIncomingMessage(client(url), "", ClosedMessage("sub", "auth-required: subscribers only")) + val obs = closed.only() + assertTrue(obs.authRequired) + assertEquals("auth-required", obs.closedReason) + } @Test - fun `a CLOSED that is not about auth is categorised rather than misread`() { - val o = RelayObserver() - o.onIncomingMessage(client(url), "", ClosedMessage("sub", "rate-limited: slow down")) + fun `a CLOSED that is not about auth is categorised rather than misread`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onIncomingMessage(client(url), "", ClosedMessage("sub", "rate-limited: slow down")) - val obs = o.only() - assertEquals("rate-limited", obs.closedReason) - assertFalse(obs.authRequired, "only an auth refusal means auth is required") - } + val obs = o.only() + assertEquals("rate-limited", obs.closedReason) + assertFalse(obs.authRequired, "only an auth refusal means auth is required") + } // ---- publishing bookkeeping --------------------------------------------- @Test - fun `an unchanged relay is not re-reported but its measurement survives`() { - // Re-writing a record refreshes its freshness window, so a relay nobody - // re-measured must be left out. But the measurement itself has to stay: - // a long-lived socket fires onConnected once, and if publishing erased - // it, the relays we know best would be the ones we could never describe - // again. - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) + fun `an unchanged relay is not re-reported but its measurement survives`() = + kotlinx.coroutines.test.runTest { + // Re-writing a record refreshes its freshness window, so a relay nobody + // re-measured must be left out. But the measurement itself has to stay: + // a long-lived socket fires onConnected once, and if publishing erased + // it, the relays we know best would be the ones we could never describe + // again. + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) - val first = o.collectUnreported().single() - assertNotNull(first.rttOpenMs) - assertEquals(0, o.collectUnreported().size, "nothing new to say") + val first = o.collectUnreported().single() + assertNotNull(first.rttOpenMs) + assertEquals(0, o.collectUnreported().size, "nothing new to say") - o.onIncomingMessage(client(url), "", NoticeMessage("slow down")) - val second = o.collectUnreported().single() - assertEquals(first.rttOpenMs, second.rttOpenMs, "the last real measurement still stands") - } + o.onIncomingMessage(client(url), "", NoticeMessage("slow down")) + val second = o.collectUnreported().single() + assertEquals(first.rttOpenMs, second.rttOpenMs, "the last real measurement still stands") + } // ---- findings from outside the websocket client ------------------------ @Test - fun `a probe failure is published even though nothing was dialled`() { - // The cheap checks that decide NOT to open a websocket are exactly the - // ones that learn a relay is gone. Without a way in, a listener-only - // observer reports on the small minority it happened to connect to — - // 104 records out of a 16,507-relay list — which is not a census. - val o = RelayObserver() - o.record(url, reachable = false, error = "nodename nor servname provided") + fun `a probe failure is published even though nothing was dialled`() = + kotlinx.coroutines.test.runTest { + // The cheap checks that decide NOT to open a websocket are exactly the + // ones that learn a relay is gone. Without a way in, a listener-only + // observer reports on the small minority it happened to connect to — + // 104 records out of a 16,507-relay list — which is not a census. + val o = RelayObserver() + o.record(url, reachable = false, error = "nodename nor servname provided") - val obs = o.only() - assertFalse(obs.reachable) - assertEquals("nodename nor servname provided", obs.error) - assertNull(obs.rttOpenMs, "a failed probe times nothing") - } + val obs = o.only() + assertFalse(obs.reachable) + assertEquals("nodename nor servname provided", obs.error) + assertNull(obs.rttOpenMs, "a failed probe times nothing") + } @Test - fun `a probe that connected reports its measured time or none at all`() { - val timed = RelayObserver() - timed.record(url, reachable = true, rttOpenMs = 42) - assertEquals(42L, timed.only().rttOpenMs) + fun `a probe that connected reports its measured time or none at all`() = + kotlinx.coroutines.test.runTest { + val timed = RelayObserver() + timed.record(url, reachable = true, rttOpenMs = 42) + assertEquals(42L, timed.only().rttOpenMs) - val untimed = RelayObserver() - untimed.record(url, reachable = true) - val obs = untimed.only() - assertTrue(obs.reachable) - assertNull(obs.rttOpenMs, "reachable without a timing must not invent one") - } + val untimed = RelayObserver() + untimed.record(url, reachable = true) + val obs = untimed.only() + assertTrue(obs.reachable) + assertNull(obs.rttOpenMs, "reachable without a timing must not invent one") + } @Test - fun `a failed probe does not demote a relay that already answered`() { - // Same rule the connection path follows: one bad probe is not death, and - // only the writer decides what record a mixed history becomes. - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.record(url, reachable = false, error = "connect timeout") + fun `a failed probe does not demote a relay that already answered`() = + kotlinx.coroutines.test.runTest { + // Same rule the connection path follows: one bad probe is not death, and + // only the writer decides what record a mixed history becomes. + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.record(url, reachable = false, error = "connect timeout") - assertTrue(o.only().reachable, "it answered; a later probe failure does not erase that") - } + assertTrue(o.only().reachable, "it answered; a later probe failure does not erase that") + } @Test - fun `an out-of-band finding is reported once like any other`() { - val o = RelayObserver() - o.record(url, reachable = false, error = "refused") - assertEquals(1, o.collectUnreported().size) - assertEquals(0, o.collectUnreported().size, "nothing new to say") - } + fun `an out-of-band finding is reported once like any other`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.record(url, reachable = false, error = "refused") + assertEquals(1, o.collectUnreported().size) + assertEquals(0, o.collectUnreported().size, "nothing new to say") + } @Test - fun `each relay is observed on its own`() { - val o = RelayObserver() - o.onConnecting(client(url)) - o.onConnected(client(url), 1, true) - o.onConnecting(client(other)) - o.onCannotConnect(client(other), "nodename nor servname provided") + fun `each relay is observed on its own`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + o.onConnecting(client(url)) + o.onConnected(client(url), 1, true) + o.onConnecting(client(other)) + o.onCannotConnect(client(other), "nodename nor servname provided") - val byUrl = o.collectUnreported().associateBy { it.url } - assertTrue(byUrl.getValue(url).reachable) - assertFalse(byUrl.getValue(other).reachable) - } + val byUrl = o.collectUnreported().associateBy { it.url } + assertTrue(byUrl.getValue(url).reachable) + assertFalse(byUrl.getValue(other).reachable) + } // ---- the run-level summary (what RelayDiagnostics used to give) ----------- @Test - fun `the summary tallies feedback across every relay`() { - val o = RelayObserver() - assertFalse(o.hadFeedback()) + fun `the summary tallies feedback across every relay`() = + kotlinx.coroutines.test.runTest { + val o = RelayObserver() + assertFalse(o.hadFeedback()) - o.onIncomingMessage(client(url), "", AuthMessage("c1")) - o.onIncomingMessage(client(other), "", AuthMessage("c2")) - o.onIncomingMessage(client(url), "", ClosedMessage("s", "rate-limited: slow")) - o.onIncomingMessage(client(other), "", ClosedMessage("s", "rate-limited: slow")) - o.onIncomingMessage(client(url), "", NoticeMessage("too many REQs")) + o.onIncomingMessage(client(url), "", AuthMessage("c1")) + o.onIncomingMessage(client(other), "", AuthMessage("c2")) + o.onIncomingMessage(client(url), "", ClosedMessage("s", "rate-limited: slow")) + o.onIncomingMessage(client(other), "", ClosedMessage("s", "rate-limited: slow")) + o.onIncomingMessage(client(url), "", NoticeMessage("too many REQs")) - assertTrue(o.hadFeedback()) - val s = o.summary() - assertEquals(2L, s["auth_challenges"]) - assertEquals(2, s["auth_required_relays"]) - assertEquals(mapOf("rate-limited" to 2L), s["closed_by_reason"]) - assertEquals(1L, s["notices"]) - } + assertTrue(o.hadFeedback()) + val s = o.summary() + assertEquals(2L, s["auth_challenges"]) + assertEquals(2, s["auth_required_relays"]) + assertEquals(mapOf("rate-limited" to 2L), s["closed_by_reason"]) + assertEquals(1L, s["notices"]) + } @Test - fun `the summary outlives publishing`() { - // It answers "how did this run go", which must not be reset by the - // unrelated act of writing records out. - val o = RelayObserver() - o.onIncomingMessage(client(url), "", AuthMessage("c")) - o.collectUnreported() + fun `the summary outlives publishing`() = + kotlinx.coroutines.test.runTest { + // It answers "how did this run go", which must not be reset by the + // unrelated act of writing records out. + val o = RelayObserver() + o.onIncomingMessage(client(url), "", AuthMessage("c")) + o.collectUnreported() - assertTrue(o.hadFeedback(), "a flush must not erase the run's tally") - assertEquals(1L, o.summary()["auth_challenges"]) - } + assertTrue(o.hadFeedback(), "a flush must not erase the run's tally") + assertEquals(1L, o.summary()["auth_challenges"]) + } @Test - fun `a machine-readable prefix is extracted or falls back to other`() { - assertEquals("auth-required", RelayObserver.prefixOf("auth-required: come back signed")) - assertEquals("other", RelayObserver.prefixOf("just some prose")) - assertEquals("other", RelayObserver.prefixOf("")) - } + fun `a machine-readable prefix is extracted or falls back to other`() = + kotlinx.coroutines.test.runTest { + assertEquals("auth-required", RelayObserver.prefixOf("auth-required: come back signed")) + assertEquals("other", RelayObserver.prefixOf("just some prose")) + assertEquals("other", RelayObserver.prefixOf("")) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProberFlowTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProberFlowTest.kt index 8593489ab2..1cfdfcea00 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProberFlowTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/reachability/RelayProberFlowTest.kt @@ -85,7 +85,7 @@ class RelayProberFlowTest { } /** Plays a relay's OK answer for the published event to every armed listener. */ - fun answerOk( + suspend fun answerOk( relay: NormalizedRelayUrl, success: Boolean, message: String, @@ -235,13 +235,14 @@ class RelayProberFlowTest { } @Test - fun writeTestEventIsEphemeralAndSelfExpiring() { - val template = RelayProbeWriteTest.build(createdAt = 5000) + fun writeTestEventIsEphemeralAndSelfExpiring() = + kotlinx.coroutines.test.runTest { + val template = RelayProbeWriteTest.build(createdAt = 5000) - assertEquals(20166, template.kind) - assertTrue(template.kind in 20000..29999, "the write probe must be an ephemeral kind") - assertTrue(listOf("expiration", "5060") in template.tags.map { it.toList() }) - } + assertEquals(20166, template.kind) + assertTrue(template.kind in 20000..29999, "the write probe must be an ephemeral kind") + assertTrue(listOf("expiration", "5060") in template.tags.map { it.toList() }) + } // ------------------------------------------------------------------ // readWriteCheck — honest read + write measurements, nothing claimed @@ -358,108 +359,117 @@ class RelayProberFlowTest { private fun tagsOf(template: EventTemplate<*>) = template.tags.map { it.toList() } @Test - fun reachableVerdictTemplateCarriesLivenessAndNetwork() { - val template = - RelayProber - .Verdict(fast, reachable = true, rttOpenMs = 150, rttEoseMs = 480, error = null) - .toDiscoveryEventTemplate(createdAt = 1000) + fun reachableVerdictTemplateCarriesLivenessAndNetwork() = + kotlinx.coroutines.test.runTest { + val template = + RelayProber + .Verdict(fast, reachable = true, rttOpenMs = 150, rttEoseMs = 480, error = null) + .toDiscoveryEventTemplate(createdAt = 1000) - val tags = tagsOf(template) - assertEquals(30166, template.kind) - assertEquals(1000, template.createdAt) - assertTrue(listOf("d", fast.url) in tags) - assertTrue(listOf("n", "clearnet") in tags) - assertTrue(listOf("rtt-open", "150") in tags) - // rtt-eose is wave-relative (dial + queue + read) — never published as rtt-read. - assertNull(tags.firstOrNull { it[0] == "rtt-read" }) - } + val tags = tagsOf(template) + assertEquals(30166, template.kind) + assertEquals(1000, template.createdAt) + assertTrue(listOf("d", fast.url) in tags) + assertTrue(listOf("n", "clearnet") in tags) + assertTrue(listOf("rtt-open", "150") in tags) + // rtt-eose is wave-relative (dial + queue + read) — never published as rtt-read. + assertNull(tags.firstOrNull { it[0] == "rtt-read" }) + } @Test - fun deadVerdictTemplateHasNoRttOpen() { - val template = - RelayProber - .Verdict(silent, reachable = false, rttOpenMs = -1, rttEoseMs = -1, error = "cannot:timeout") - .toDiscoveryEventTemplate() + fun deadVerdictTemplateHasNoRttOpen() = + kotlinx.coroutines.test.runTest { + val template = + RelayProber + .Verdict(silent, reachable = false, rttOpenMs = -1, rttEoseMs = -1, error = "cannot:timeout") + .toDiscoveryEventTemplate() - val tags = tagsOf(template) - assertTrue(listOf("d", silent.url) in tags) - // Liveness is the PRESENCE of rtt-open; a dead record must not carry one. - assertNull(tags.firstOrNull { it[0] == "rtt-open" }) - } + val tags = tagsOf(template) + assertTrue(listOf("d", silent.url) in tags) + // Liveness is the PRESENCE of rtt-open; a dead record must not carry one. + assertNull(tags.firstOrNull { it[0] == "rtt-open" }) + } @Test - fun reachableWithoutMeasuredLatencyWritesZeroFlag() { - val template = - RelayProber - .Verdict(fast, reachable = true, rttOpenMs = -1, rttEoseMs = 300, error = null) - .toDiscoveryEventTemplate() + fun reachableWithoutMeasuredLatencyWritesZeroFlag() = + kotlinx.coroutines.test.runTest { + val template = + RelayProber + .Verdict(fast, reachable = true, rttOpenMs = -1, rttEoseMs = 300, error = null) + .toDiscoveryEventTemplate() - // 0 = "reachable, latency not observed": the flag form, never an invented number. - assertTrue(listOf("rtt-open", "0") in tagsOf(template)) - } + // 0 = "reachable, latency not observed": the flag form, never an invented number. + assertTrue(listOf("rtt-open", "0") in tagsOf(template)) + } @Test - fun observedAuthWallBecomesARequirementTag() { - val template = - RelayProber - .Verdict(walled, reachable = true, rttOpenMs = 90, rttEoseMs = -1, error = "closed:auth-required: sign in") - .toDiscoveryEventTemplate() + fun observedAuthWallBecomesARequirementTag() = + kotlinx.coroutines.test.runTest { + val template = + RelayProber + .Verdict(walled, reachable = true, rttOpenMs = 90, rttEoseMs = -1, error = "closed:auth-required: sign in") + .toDiscoveryEventTemplate() - assertTrue(listOf("R", "auth") in tagsOf(template)) - } + assertTrue(listOf("R", "auth") in tagsOf(template)) + } @Test - fun policyClosedIsNotAnAuthRequirement() { - val template = - RelayProber - .Verdict(walled, reachable = true, rttOpenMs = 90, rttEoseMs = -1, error = "closed:blocked: not welcome") - .toDiscoveryEventTemplate() + fun policyClosedIsNotAnAuthRequirement() = + kotlinx.coroutines.test.runTest { + val template = + RelayProber + .Verdict(walled, reachable = true, rttOpenMs = 90, rttEoseMs = -1, error = "closed:blocked: not welcome") + .toDiscoveryEventTemplate() - assertNull(tagsOf(template).firstOrNull { it[0] == "R" }) - } + assertNull(tagsOf(template).firstOrNull { it[0] == "R" }) + } @Test - fun readWriteResultsBecomeRttTags() { - val verdict = RelayProber.Verdict(fast, reachable = true, rttOpenMs = 100, rttEoseMs = 300, error = null) - val readWrite = RelayProber.ReadWriteVerdict(fast, rttReadMs = 40, rttWriteMs = 55, writeAccepted = true, writeMessage = "") + fun readWriteResultsBecomeRttTags() = + kotlinx.coroutines.test.runTest { + val verdict = RelayProber.Verdict(fast, reachable = true, rttOpenMs = 100, rttEoseMs = 300, error = null) + val readWrite = RelayProber.ReadWriteVerdict(fast, rttReadMs = 40, rttWriteMs = 55, writeAccepted = true, writeMessage = "") - val tags = tagsOf(verdict.toDiscoveryEventTemplate(readWrite = readWrite)) - assertTrue(listOf("rtt-read", "40") in tags) - assertTrue(listOf("rtt-write", "55") in tags) - } + val tags = tagsOf(verdict.toDiscoveryEventTemplate(readWrite = readWrite)) + assertTrue(listOf("rtt-read", "40") in tags) + assertTrue(listOf("rtt-write", "55") in tags) + } @Test - fun unobservedReadWriteSidesStayUntagged() { - val verdict = RelayProber.Verdict(fast, reachable = true, rttOpenMs = 100, rttEoseMs = -1, error = null) - val readWrite = RelayProber.ReadWriteVerdict(fast, rttReadMs = -1, rttWriteMs = -1, writeAccepted = null, writeMessage = null) + fun unobservedReadWriteSidesStayUntagged() = + kotlinx.coroutines.test.runTest { + val verdict = RelayProber.Verdict(fast, reachable = true, rttOpenMs = 100, rttEoseMs = -1, error = null) + val readWrite = RelayProber.ReadWriteVerdict(fast, rttReadMs = -1, rttWriteMs = -1, writeAccepted = null, writeMessage = null) - val tags = tagsOf(verdict.toDiscoveryEventTemplate(readWrite = readWrite)) - assertNull(tags.firstOrNull { it[0] == "rtt-read" }) - assertNull(tags.firstOrNull { it[0] == "rtt-write" }) - } + val tags = tagsOf(verdict.toDiscoveryEventTemplate(readWrite = readWrite)) + assertNull(tags.firstOrNull { it[0] == "rtt-read" }) + assertNull(tags.firstOrNull { it[0] == "rtt-write" }) + } @Test - fun writeRejectionReasonsBecomeRequirementTags() { - val verdict = RelayProber.Verdict(walled, reachable = true, rttOpenMs = 100, rttEoseMs = -1, error = null) + fun writeRejectionReasonsBecomeRequirementTags() = + kotlinx.coroutines.test.runTest { + val verdict = RelayProber.Verdict(walled, reachable = true, rttOpenMs = 100, rttEoseMs = -1, error = null) - val pow = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "pow: 28 bits needed") - assertTrue(listOf("R", "pow") in tagsOf(verdict.toDiscoveryEventTemplate(readWrite = pow))) + val pow = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "pow: 28 bits needed") + assertTrue(listOf("R", "pow") in tagsOf(verdict.toDiscoveryEventTemplate(readWrite = pow))) - val auth = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "auth-required: sign in") - assertTrue(listOf("R", "auth") in tagsOf(verdict.toDiscoveryEventTemplate(readWrite = auth))) + val auth = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "auth-required: sign in") + assertTrue(listOf("R", "auth") in tagsOf(verdict.toDiscoveryEventTemplate(readWrite = auth))) - val blocked = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "blocked: not welcome") - assertNull(tagsOf(verdict.toDiscoveryEventTemplate(readWrite = blocked)).firstOrNull { it[0] == "R" }) - } + val blocked = RelayProber.ReadWriteVerdict(walled, -1, 30, writeAccepted = false, writeMessage = "blocked: not welcome") + assertNull(tagsOf(verdict.toDiscoveryEventTemplate(readWrite = blocked)).firstOrNull { it[0] == "R" }) + } @Test - fun onionRelayIsTaggedTor() { - val onion = RelayUrlNormalizer.normalize("ws://someonionaddressabcdefghijklmnop.onion") - val template = - RelayProber - .Verdict(onion, reachable = true, rttOpenMs = 900, rttEoseMs = -1, error = null) - .toDiscoveryEventTemplate() + fun onionRelayIsTaggedTor() = + kotlinx.coroutines.test.runTest { + val onion = RelayUrlNormalizer.normalize("ws://someonionaddressabcdefghijklmnop.onion") + val template = + RelayProber + .Verdict(onion, reachable = true, rttOpenMs = 900, rttEoseMs = -1, error = null) + .toDiscoveryEventTemplate() - assertTrue(listOf("n", "tor") in tagsOf(template)) - } + assertTrue(listOf("n", "tor") in tagsOf(template)) + } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt index 06354f98d2..50dfac2519 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt @@ -47,7 +47,7 @@ class NostrClientManualSubTest : RelayClientTest() { val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt index 0c283d4314..7531b70068 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt @@ -71,7 +71,7 @@ class NostrClientRepeatSubTest : RelayClientTest() { val listener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/PoolRequestsConcurrencyTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/PoolRequestsConcurrencyTest.kt index 6d21d526e3..4add1aea15 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/PoolRequestsConcurrencyTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/PoolRequestsConcurrencyTest.kt @@ -78,69 +78,70 @@ class PoolRequestsConcurrencyTest { } @Test - fun concurrentEoseResendAndSubscribeSendExactlyOneReq() { - val url = RelayUrlNormalizer.normalize("ws://race/") - val subId = "shared-sub" - val filtersA = listOf(Filter(kinds = listOf(1))) - val filtersB = listOf(Filter(kinds = listOf(2))) - val listener = object : SubscriptionListener {} + fun concurrentEoseResendAndSubscribeSendExactlyOneReq() = + kotlinx.coroutines.test.runTest { + val url = RelayUrlNormalizer.normalize("ws://race/") + val subId = "shared-sub" + val filtersA = listOf(Filter(kinds = listOf(1))) + val filtersB = listOf(Filter(kinds = listOf(2))) + val listener = object : SubscriptionListener {} - // Many episodes so a regression that only sometimes doubles still trips. - repeat(300) { episode -> - val pool = PoolRequests() - val reqBCount = AtomicInteger(0) + // Many episodes so a regression that only sometimes doubles still trips. + repeat(300) { episode -> + val pool = PoolRequests() + val reqBCount = AtomicInteger(0) - fun countReqB(cmd: Command) { - if (cmd is ReqCmd && cmd.filters == filtersB) reqBCount.incrementAndGet() - } - - val fakeRelay = - FakeRelay(url) { cmd -> - // relay-reader auto-resend send path - countReqB(cmd) - pool.onSent(url, cmd) + fun countReqB(cmd: Command) { + if (cmd is ReqCmd && cmd.filters == filtersB) reqBCount.incrementAndGet() } - // Bring the sub to LIVE with filters A. - val setupRelays = pool.addOrUpdate(subId, mapOf(url to filtersA), listener) - pool.sendToRelayIfChanged(subId, setupRelays) { _, cmd -> pool.onSent(url, cmd) } - pool.onIncomingMessage(fakeRelay, EoseMessage(subId)) - - // The desired filters change to B (e.g. the next page of a paged download). - pool.addOrUpdate(subId, mapOf(url to filtersB), listener) - - val appProducedReq = CountDownLatch(1) - val readerDone = CountDownLatch(1) - - val appThread = - thread { - pool.sendToRelayIfChanged(subId, setOf(url)) { _, cmd -> + val fakeRelay = + FakeRelay(url) { cmd -> + // relay-reader auto-resend send path countReqB(cmd) - // App has produced its REQ(B); park before onSent so the - // subscription state is not yet advanced — the exact window - // the race needs. - appProducedReq.countDown() - readerDone.await() pool.onSent(url, cmd) } - } - val readerThread = - thread { - appProducedReq.await() - pool.onIncomingMessage(fakeRelay, EoseMessage(subId)) - readerDone.countDown() - } + // Bring the sub to LIVE with filters A. + val setupRelays = pool.addOrUpdate(subId, mapOf(url to filtersA), listener) + pool.sendToRelayIfChanged(subId, setupRelays) { _, cmd -> pool.onSent(url, cmd) } + pool.onIncomingMessage(fakeRelay, EoseMessage(subId)) - appThread.join() - readerThread.join() + // The desired filters change to B (e.g. the next page of a paged download). + pool.addOrUpdate(subId, mapOf(url to filtersB), listener) - assertEquals( - 1, - reqBCount.get(), - "episode $episode: exactly one REQ must be sent for the changed filters, " + - "never a duplicate from the app + reader race", - ) + val appProducedReq = CountDownLatch(1) + val readerDone = CountDownLatch(1) + + val appThread = + thread { + pool.sendToRelayIfChanged(subId, setOf(url)) { _, cmd -> + countReqB(cmd) + // App has produced its REQ(B); park before onSent so the + // subscription state is not yet advanced — the exact window + // the race needs. + appProducedReq.countDown() + readerDone.await() + pool.onSent(url, cmd) + } + } + + val readerThread = + thread { + appProducedReq.await() + kotlinx.coroutines.runBlocking { pool.onIncomingMessage(fakeRelay, EoseMessage(subId)) } + readerDone.countDown() + } + + appThread.join() + readerThread.join() + + assertEquals( + 1, + reqBCount.get(), + "episode $episode: exactly one REQ must be sent for the changed filters, " + + "never a duplicate from the app + reader race", + ) + } } - } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticatorReauthOnClosedTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticatorReauthOnClosedTest.kt index e10a5a7c32..e8ae39933b 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticatorReauthOnClosedTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticatorReauthOnClosedTest.kt @@ -100,7 +100,7 @@ class RelayAuthenticatorReauthOnClosedTest { .filterIsInstance() .last() .event - listener.onIncomingMessage(relay, "", OkMessage.accepted(newest.id)) + kotlinx.coroutines.runBlocking { listener.onIncomingMessage(relay, "", OkMessage.accepted(newest.id)) } } @Test diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NegentropyRejectionFallbackTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NegentropyRejectionFallbackTest.kt index 230674d4d0..006b8ffe16 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NegentropyRejectionFallbackTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NegentropyRejectionFallbackTest.kt @@ -91,11 +91,11 @@ class NegentropyRejectionFallbackTest { when { // The keep-alive REQ and any paging REQ: answer EOSE so the // subscription settles (paging then completes with 0 events). - msg.startsWith("[\"REQ\"") -> subIdOf(msg)?.let { out.onMessage("[\"EOSE\",\"$it\"]") } + msg.startsWith("[\"REQ\"") -> subIdOf(msg)?.let { kotlinx.coroutines.runBlocking { out.onMessage("[\"EOSE\",\"$it\"]") } } // The negentropy handshake: the relay refuses. msg.startsWith("[\"NEG-OPEN\"") -> { negOpens.incrementAndGet() - subIdOf(msg)?.let { out.onMessage(replyToNegOpen(it)) } + subIdOf(msg)?.let { kotlinx.coroutines.runBlocking { out.onMessage(replyToNegOpen(it)) } } } else -> Unit } @@ -140,25 +140,28 @@ class NegentropyRejectionFallbackTest { } @Test - fun strfryNegentropyDisabledFallsBackToPaging() { - negOpenRejectedBy { "[\"NOTICE\",\"ERROR: bad msg: negentropy disabled\"]" } - } + fun strfryNegentropyDisabledFallsBackToPaging() = + kotlinx.coroutines.test.runTest { + negOpenRejectedBy { "[\"NOTICE\",\"ERROR: bad msg: negentropy disabled\"]" } + } @Test - fun purplePagesUnknownEnvelopeFallsBackToPaging() { - negOpenRejectedBy { "[\"NOTICE\",\"failed to parse envelope: unknown envelope label\"]" } - } + fun purplePagesUnknownEnvelopeFallsBackToPaging() = + kotlinx.coroutines.test.runTest { + negOpenRejectedBy { "[\"NOTICE\",\"failed to parse envelope: unknown envelope label\"]" } + } @Test - fun rateLimitNegErrPagesWithoutSplitStorm() { - // A NEG-ERR that does NOT shrink with the window ("too many requests") must not - // be mistaken for a set-too-large overflow: doing so would binary-split the - // created_at range forever. Assert we page after exactly ONE NEG-OPEN. - val relay = negOpenRejectedBy { subId -> "[\"NEG-ERR\",\"$subId\",\"rate-limited: too many requests\"]" } - assertEquals( - 2, - relay.negOpens.get(), - "one NEG-OPEN per phase (sync + syncOrFetch), i.e. no window-split storm; got ${relay.negOpens.get()}", - ) - } + fun rateLimitNegErrPagesWithoutSplitStorm() = + kotlinx.coroutines.test.runTest { + // A NEG-ERR that does NOT shrink with the window ("too many requests") must not + // be mistaken for a set-too-large overflow: doing so would binary-split the + // created_at range forever. Assert we page after exactly ONE NEG-OPEN. + val relay = negOpenRejectedBy { subId -> "[\"NEG-ERR\",\"$subId\",\"rate-limited: too many requests\"]" } + assertEquals( + 2, + relay.negOpens.get(), + "one NEG-OPEN per phase (sync + syncOrFetch), i.e. no window-split storm; got ${relay.negOpens.get()}", + ) + } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ByIdFetchBenchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ByIdFetchBenchmark.kt index 6dd57131e0..9775a20308 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ByIdFetchBenchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ByIdFetchBenchmark.kt @@ -136,7 +136,7 @@ class ByIdFetchBenchmark { val done = Channel(Channel.CONFLATED) val listener = object : SubscriptionListener { - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl, diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/DispatchStageBenchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/DispatchStageBenchmark.kt index 8a64c4a0e5..ed0909257e 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/DispatchStageBenchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/DispatchStageBenchmark.kt @@ -197,7 +197,7 @@ class DispatchStageBenchmark { continue } for (subId in subIds) { - client.onIncomingMessage(relayClient, "", EventMessage(subId, event)) + kotlinx.coroutines.runBlocking { client.onIncomingMessage(relayClient, "", EventMessage(subId, event)) } } } } @@ -259,7 +259,7 @@ class DispatchStageBenchmark { Thread { for (event in events) { for (subId in subIds) { - pool.onIncomingMessage(relayClient, EventMessage(subId, event)) + kotlinx.coroutines.runBlocking { pool.onIncomingMessage(relayClient, EventMessage(subId, event)) } } } } @@ -299,12 +299,13 @@ class DispatchStageBenchmark { } @Test - fun dispatchStageBenchmark() { - println("=== DISPATCH STAGE BENCHMARK (post-parse, pre-verify) ===") - println("cores=${Runtime.getRuntime().availableProcessors()} uniqueEvents=$UNIQUE_EVENTS subsPerRelay=$SUBS_PER_RELAY") + fun dispatchStageBenchmark() = + kotlinx.coroutines.test.runTest { + println("=== DISPATCH STAGE BENCHMARK (post-parse, pre-verify) ===") + println("cores=${Runtime.getRuntime().availableProcessors()} uniqueEvents=$UNIQUE_EVENTS subsPerRelay=$SUBS_PER_RELAY") - // warmup pass (JIT), then the measured pass - runAllVariants(print = false) - runAllVariants(print = true) - } + // warmup pass (JIT), then the measured pass + runAllVariants(print = false) + runAllVariants(print = true) + } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyMultiRelayLiveTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyMultiRelayLiveTest.kt index 1c72b53690..8d95420c6b 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyMultiRelayLiveTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyMultiRelayLiveTest.kt @@ -177,7 +177,7 @@ class NegentropyMultiRelayLiveTest { val listener = object : RelayConnectionListener { - override fun onIncomingMessage( + override suspend fun onIncomingMessage( relay: IRelayClient, msgStr: String, msg: Message, diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyStallRepro.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyStallRepro.kt index 04d6338582..c85e4fb80a 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyStallRepro.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/NegentropyStallRepro.kt @@ -109,7 +109,7 @@ class NegentropyStallRepro { out.onOpen(pingMillis, usingCompression) } - override fun onMessage(text: String) { + override suspend fun onMessage(text: String) { val t = tagger(text) recvCounts.getOrPut(t) { AtomicInteger() }.incrementAndGet() if (t == "NEG-MSG") negMsgBytesIn.addAndGet(text.length.toLong()) diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ProductionReceiverBenchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ProductionReceiverBenchmark.kt index b226af0641..eca181e106 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ProductionReceiverBenchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/prodbench/ProductionReceiverBenchmark.kt @@ -370,7 +370,7 @@ class ProductionReceiverBenchmark { reqSentAt.putIfAbsent(relay, System.nanoTime() - startNanos) } - override fun onEvent( + override suspend fun onEvent( event: Event, isLive: Boolean, relay: NormalizedRelayUrl,