diff --git a/quartz/README.md b/quartz/README.md index 48d4cf7e54..ef81addbe4 100644 --- a/quartz/README.md +++ b/quartz/README.md @@ -205,3 +205,96 @@ private fun NoteRow(handle: MutableStateFlow) { Notice how each how also subscribe for changes. This is important to receive updates from replaceable and addressable events. + +# Appendix A + +Quartz doesn't offer a Ktor websocket, but you can use this one as reference. + +```kotlin +/** + * Ktor-based [WebSocket] for talking to a Nostr relay. + * + * Quartz exposes [WebsocketBuilder] as the only seam between its relay-pool + * and the underlying transport, so all this class has to do is open a Ktor + * websocket session, forward incoming text frames to [out], and let Quartz + * drive sends. + */ +class KtorWebSocket( + private val url: NormalizedRelayUrl, + private val httpClient: HttpClient, + private val out: WebSocketListener, +) : WebSocket { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var session: DefaultWebSocketSession? = null + private var readerJob: Job? = null + + override fun needsReconnect(): Boolean = session == null + + override fun connect() { + readerJob = + scope.launch { + try { + val s = httpClient.webSocketSession(urlString = url.url) + session = s + out.onOpen(0, false) + + for (frame in s.incoming) { + if (frame is Frame.Text) { + out.onMessage(frame.readText()) + } + } + + val reason = s.closeReason.await() + out.onClosed( + code = + reason?.code?.toInt() ?: CloseReason.Codes.NORMAL.code + .toInt(), + reason = reason?.message ?: "", + ) + } catch (t: Throwable) { + out.onFailure(t, null, null) + } finally { + session = null + } + } + } + + override fun disconnect() { + val s = session + session = null + readerJob?.cancel() + readerJob = null + if (s != null) { + runBlocking { s.close(CloseReason(CloseReason.Codes.NORMAL, "client disconnect")) } + } + scope.cancel() + } + + override fun send(msg: String): Boolean { + val s = session ?: return false + scope.launch { s.send(msg) } + return true + } + + /** + * The factory Quartz hands to [com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient]. + * One [HttpClient] is shared by every relay in the pool. + */ + class Builder( + private val httpClient: HttpClient = defaultClient(), + ) : WebsocketBuilder { + override fun build( + url: NormalizedRelayUrl, + out: WebSocketListener, + ): WebSocket = KtorWebSocket(url, httpClient, out) + + companion object { + fun defaultClient() = + HttpClient(CIO) { + install(WebSockets) + } + } + } +} +``` +