chore(audio-rooms): align catalog/announces error timing + comment (audit #5-7)
* 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.
This commit is contained in:
+5
-4
@@ -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,
|
||||
|
||||
@@ -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<RoomAnnouncement> =
|
||||
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()
|
||||
|
||||
+6
-6
@@ -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<UnsupportedOperationException> {
|
||||
flow.collect { /* unreachable */ }
|
||||
listener.announces()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user