From 43c5313674fff28ba3813d0d4f9f91039d8b4ed3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 02:28:24 +0000 Subject: [PATCH] chore(audio-rooms): align catalog/announces error timing + comment (audit #5-7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * announces() now throws UnsupportedOperationException at CALL time (not the first collect) on the IETF default — matches subscribeCatalog's timing so both can be guarded with one `runCatching { listener.announces() }` per session rather than a runCatching around every collect site (audit #6). * KDoc on announces() documents the deliberate hot-vs-cold asymmetry with subscribeSpeaker/subscribeCatalog: announce data is room-state with a single VM consumer (cold flow is sufficient), audio is per-frame playout with multi-collect resilience needs (hot SharedFlow with DROP_OLDEST). The different shapes are intentional, not accidental (audit #5). * MoqLiteNestsListener.wrapSubscription's "IETF SubscribeHandle path conventionally surfaces" comment was scoped to the audio track when first written; now the same body serves both audio and catalog. Re-frame so the comment applies to either track (audit #7). Test: NestsListenerCatalogTest's announces-throws case now asserts the call itself throws, not the collect. --- .../nestsclient/MoqLiteNestsListener.kt | 9 ++++--- .../nestsclient/NestsListener.kt | 25 +++++++++++-------- .../nestsclient/NestsListenerCatalogTest.kt | 12 ++++----- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsListener.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsListener.kt index 51b61c843..01db9a504 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsListener.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsListener.kt @@ -80,10 +80,11 @@ class MoqLiteNestsListener internal constructor( try { session.subscribe(broadcast = broadcast, track = track) } catch (e: MoqLiteSubscribeException) { - // The IETF SubscribeHandle path conventionally surfaces - // protocol-level rejections through the same exception - // shape MoqProtocolException uses, so wrapping here - // avoids leaking moq-lite types into UI consumers. + // Surface protocol-level rejections (audio OR catalog) + // through MoqProtocolException, matching the IETF + // listener's contract — keeps moq-lite types out of + // UI / VM consumers regardless of which track was + // refused. throw com.vitorpamplona.nestsclient.moq.MoqProtocolException( "moq-lite subscribe rejected: ${e.message}", e, diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsListener.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsListener.kt index 703674089..b9e9d3b1d 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsListener.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsListener.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flow /** * High-level listener handle for an audio-room. Hides the layered HTTP + @@ -82,20 +81,26 @@ interface NestsListener { * independent of the kind-10312 `publishing` presence tag — * the JS reference's `useRoomAnnouncements` consumes this. * - * Default body throws [UnsupportedOperationException] on the - * IETF reference path (`MoqSession` doesn't define an - * announce-prefix subscription that fits this shape). Callers - * mixing the two should `runCatching` the collect. + * The flow is COLD (re-collected starts a fresh subscription) + * — different from [subscribeSpeaker], which returns a hot + * SubscribeHandle backed by a fan-out [kotlinx.coroutines.flow.MutableSharedFlow]. + * The asymmetry is intentional: announce data is room-state + * (one consumer, no buffered replay), while audio is per-frame + * playout (multi-collect resilient, drop-oldest fan-out). + * + * Throws on the IETF reference path because moq-transport-17 + * doesn't define an announce-prefix flow. Throws at call time + * (not at first collect) so callers can `runCatching` the + * single call rather than wrapping every collect — same + * timing as [subscribeCatalog]. * * @throws UnsupportedOperationException on the IETF listener. * @throws IllegalStateException if the listener is not in [NestsListenerState.Connected]. */ fun announces(): Flow = - flow { - throw UnsupportedOperationException( - "announces() is moq-lite-only; IETF listener has no announce-prefix flow.", - ) - } + throw UnsupportedOperationException( + "announces() is moq-lite-only; IETF listener has no announce-prefix flow.", + ) /** Tear down the MoQ session + underlying transport. Idempotent. */ suspend fun close() diff --git a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/NestsListenerCatalogTest.kt b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/NestsListenerCatalogTest.kt index b9b1f1592..acece397e 100644 --- a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/NestsListenerCatalogTest.kt +++ b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/NestsListenerCatalogTest.kt @@ -55,17 +55,17 @@ class NestsListenerCatalogTest { } @Test - fun ietfDefaultListenerAnnouncesFlowThrowsOnCollect() = + fun ietfDefaultListenerAnnouncesFlowThrowsAtCallTime() = runTest { // The announce-prefix channel is moq-lite only as well — // the IETF reference path's announces() default body - // throws UnsupportedOperationException at collect time. - // Callers mixing the two protocols can `runCatching` the - // collect to no-op cleanly. + // throws UnsupportedOperationException at CALL time, + // matching subscribeCatalog's timing. Callers can + // `runCatching { listener.announces() }` once per session + // rather than wrapping every collect. val listener = IetfStyleListener() - val flow = listener.announces() assertFailsWith { - flow.collect { /* unreachable */ } + listener.announces() } } }