fix(nestsclient): pack trackPriority + sequence into stream priority — moq-lite Lite-03 priority.rs

Per moq-lite Lite-03 (kixelated/moq `rs/moq-lite/src/lite/priority.rs`),
`Publisher::serve_group` calls `priority.insert(track.priority,
sequence)` and feeds the resulting position into `stream.set_priority`.
Priority sorts first by `track.priority u8` (higher track = drains
ahead under congestion), then by group `sequence` within a track
(newer = drains ahead).

Pre-fix we passed raw `sequence.toInt()` directly to setPriority,
ignoring the per-track byte. For our single-Opus-track production case
this was unobservable — newer-first ordering held by sequence
monotonicity — but a future multi-track broadcast (audio + companion
catalog / status track) would have starved the lower-rate track the
moment audio's outbound queue got congested.

Fix: add `trackPriority: Int = DEFAULT_TRACK_PRIORITY` parameter to
`MoqLiteSession.publish()`, store on PublisherStateImpl, and bit-pack
in `openGroupStream`:
  bits 31..24  trackPriority u8 (0..255)
  bits 23..0   sequence low 24 bits

The 24-bit sequence window is ample (≈ 6 days at 1 group/sec, beyond
which all newer groups within a single track tie — but they still
beat older groups of any LOWER-priority track via the top byte).
Production sessions cycle on JWT refresh every 9 min, so the wrap is
defensive only.

`DEFAULT_TRACK_PRIORITY = 0x80` matches the existing subscriber-side
DEFAULT_PRIORITY midpoint, so all existing call sites keep their
prior behavior.

Test seam: `FakeWebTransport.openUniStream` now records the most-
recent `setPriority` value via a shared `AtomicInteger` cell that the
peer-side `FakeReadStream` exposes as `lastSetPriority`. Lets the new
regression test verify the bit-pack formula on the actual peer-side
read stream rather than peeking into private state.

Regression test:
`MoqLiteSessionTest.publisher_packs_trackPriority_and_sequence_into_setPriority_value`.

Audit doc: nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md (M1).

https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
This commit is contained in:
Claude
2026-05-09 14:16:36 +00:00
parent 626521a6f2
commit 38df70504a
3 changed files with 159 additions and 12 deletions
@@ -21,6 +21,7 @@
package com.vitorpamplona.nestsclient.moq.lite
import com.vitorpamplona.nestsclient.transport.FakeBidiStream
import com.vitorpamplona.nestsclient.transport.FakeReadStream
import com.vitorpamplona.nestsclient.transport.FakeWebTransport
import com.vitorpamplona.quic.Varint
import kotlinx.coroutines.CoroutineScope
@@ -399,6 +400,68 @@ class MoqLiteSessionTest {
session.close()
}
@Test
fun publisher_packs_trackPriority_and_sequence_into_setPriority_value() =
runBlocking {
// Lite-03 audit M1: openGroupStream packs the publisher's
// trackPriority byte into bits 31..24 and the group sequence
// into bits 23..0 of the value passed to
// WebTransportWriteStream.setPriority. This mirrors the
// (track.priority, sequence) ordering kixelated's
// PriorityHandle uses at `rs/moq-lite/src/lite/priority.rs`.
// Verified on the *peer* side via FakeReadStream.lastSetPriority,
// which the in-memory transport uses as a side-channel for
// priority introspection.
val (clientSide, serverSide) = FakeWebTransport.pair()
val session = MoqLiteSession.client(clientSide, pumpScope)
// Use a non-default trackPriority so a regression that
// accidentally drops it back to DEFAULT_TRACK_PRIORITY shows
// up in the assertion.
val publisher =
session.publish(
broadcastSuffix = "speakerPubkey",
track = "audio/data",
startSequence = 7L,
trackPriority = 0xC0,
)
val subBidi = serverSide.openBidiStream()
subBidi.write(Varint.encode(MoqLiteControlType.Subscribe.code))
subBidi.write(
MoqLiteCodec.encodeSubscribe(
MoqLiteSubscribe(
id = 99L,
broadcast = "speakerPubkey",
track = "audio/data",
priority = 0x80,
ordered = true,
maxLatencyMillis = 0L,
startGroup = null,
endGroup = null,
),
),
)
withTimeout(2_000) { subBidi.incoming().first() }
assertEquals(true, publisher.send("opus-frame".encodeToByteArray()))
publisher.endGroup()
val relayUni = withTimeout(2_000) { serverSide.incomingUniStreams().first() }
val fakeRead = relayUni as FakeReadStream
// Expected: (0xC0 shl 24) or (7 and 0x00FF_FFFF) =
// 0xC000_0007 in unsigned terms = -1073741817 as Int.
// Compute via the same formula so the test asserts the
// CONTRACT, not a hex literal.
val expected = ((0xC0 and 0xFF) shl 24) or (7 and 0x00FF_FFFF)
assertEquals(expected, fakeRead.lastSetPriority, "trackPriority dominates the high byte; sequence fills the low 24 bits")
// Drain the stream so the cleanup path doesn't leak.
relayUni.incoming().toList()
publisher.close()
session.close()
}
@Test
fun publisher_send_returns_false_when_no_inbound_subscriber() =
runBlocking {