fix(nestsClient): bug + perf fixes from Nests implementation review

- send() now nulls currentGroup and returns false on uni-stream write
  failure (was: silently returned true while reusing the dead stream)
- Subscribe bidi gets a single long-running collector that closes the
  frames channel on peer FIN / transport tear-down (was: consumer
  could hang forever on a black-holed UDP path with no Announce(Ended))
- AudioException.Kind gains EncoderError; mic-side encode failures no
  longer mis-route through DecoderError handlers
- MoqCodec.decode bounds the payload-length varint before .toInt() so
  a peer-driven absurd length can't wedge the parser forever
- Publish hot-path framing collapses to a single ByteArray allocation
  (was: Varint.encode + plus operator, two allocs per Opus frame at
  50 fps)
- Drops the now-unused readSubscribeResponseFromBidi /
  readSizePrefixedFromBidiInto / EarlyExit helpers

Adds a regression test that asserts the frames flow completes when
the peer FINs the subscribe bidi.

213 tests pass, 0 failures.
This commit is contained in:
Claude
2026-05-01 17:44:16 +00:00
parent 1a912e5d7c
commit eb20ac2c2a
7 changed files with 146 additions and 133 deletions
@@ -385,6 +385,34 @@ class MoqLiteSessionTest {
session.close()
}
@Test
fun frames_flow_completes_when_peer_FINs_the_subscribe_bidi() =
runBlocking {
val (clientSide, serverSide) = FakeWebTransport.pair()
val session = MoqLiteSession.client(clientSide, pumpScope)
val peerBidi =
async {
val (bidi, _) = nextSubscribeBidi(serverSide)
bidi.write(MoqLiteCodec.encodeSubscribeOk(okFor(0L)))
bidi
}
val handle = session.subscribe("speakerY", "audio/data")
val bidi = withTimeout(2_000) { peerBidi.await() }
// Peer FINs the subscribe bidi — moq-lite Lite-03's signal
// for "publisher gone, no more data ever". Without the
// per-subscription bidi-watch, this would leave the
// consumer-facing frames flow hanging forever; with the
// watch, the frames Channel closes and `toList()` returns.
bidi.finish()
withTimeout(2_000) { handle.frames.toList() }
session.close()
}
@Test
fun unsubscribe_FINs_the_subscribe_bidi() =
runBlocking {