diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSession.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSession.kt index 6fef07761..4749f5f15 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSession.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSession.kt @@ -1028,17 +1028,33 @@ class MoqLiteSession internal constructor( targetPublisher.registerInboundSubscription(sub) inboundSub = sub inboundSubPublisher = targetPublisher + // Audit L2: narrow `startGroup` to the + // publisher's [MoqLitePublisherHandle.nextSequence] + // — the exact sequence of the next group we'll + // open. Spec lets the publisher narrow the + // subscriber's bounds; pre-fix we always echoed + // `null/null`, which loses the diagnostic + // information of "which group are you about to + // start sending?" Useful for hot-swap + // continuations where `nextSequence` is non-zero + // (the seeded `startSequence` from the previous + // session). `endGroup` stays null because we're + // a live broadcast with no end in sight; the + // subscriber's request `endGroup` is honoured + // implicitly when the publisher closes. + val narrowedStart = targetPublisher.nextSequence bidi.write( MoqLiteCodec.encodeSubscribeOk( MoqLiteSubscribeOk( priority = sub.priority, ordered = sub.ordered, maxLatencyMillis = sub.maxLatencyMillis, - startGroup = null, + startGroup = narrowedStart, endGroup = null, ), ), ) + Log.d("NestTx") { "SUBSCRIBE_OK id=${sub.id} narrowed startGroup=$narrowedStart" } dispatched = true } diff --git a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSessionTest.kt b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSessionTest.kt index c51368c49..cf09542b8 100644 --- a/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSessionTest.kt +++ b/nestsClient/src/commonTest/kotlin/com/vitorpamplona/nestsclient/moq/lite/MoqLiteSessionTest.kt @@ -342,6 +342,55 @@ class MoqLiteSessionTest { session.close() } + @Test + fun publisher_subscribeOk_narrows_startGroup_to_next_sequence() = + runBlocking { + // Lite-03 audit L2: when accepting a SUBSCRIBE, the publisher + // narrows the SubscribeOk's `startGroup` from the subscriber's + // request to its own [MoqLitePublisherHandle.nextSequence], + // communicating the exact sequence the next group will + // carry. Particularly useful for hot-swap continuations + // (publisher seeded with a non-zero startSequence). + val (clientSide, serverSide) = FakeWebTransport.pair() + val session = MoqLiteSession.client(clientSide, pumpScope) + + val publisher = + session.publish( + broadcastSuffix = "speakerPubkey", + track = "audio/data", + startSequence = 100L, + ) + + val subBidi = serverSide.openBidiStream() + subBidi.write(Varint.encode(MoqLiteControlType.Subscribe.code)) + subBidi.write( + MoqLiteCodec.encodeSubscribe( + MoqLiteSubscribe( + id = 5L, + broadcast = "speakerPubkey", + track = "audio/data", + priority = 0x80, + ordered = true, + maxLatencyMillis = 0L, + // Subscriber asks "from latest" — publisher is + // free to narrow. + startGroup = null, + endGroup = null, + ), + ), + ) + + val ackChunk = withTimeout(2_000) { subBidi.incoming().first() } + val resp = MoqLiteCodec.decodeSubscribeResponse(ackChunk) + val ok = (resp as MoqLiteCodec.SubscribeResponse.Ok).ok + assertEquals(100L, ok.startGroup, "publisher narrows startGroup to its nextSequence") + // endGroup stays null — live broadcast has no end in sight. + assertEquals(null, ok.endGroup) + + publisher.close() + session.close() + } + @Test fun publisher_startSequence_seeds_first_group_for_hot_swap_continuation() = runBlocking {