5c32996f93
Lifts MoqSession from listener-only to bidirectional. A session can now
ANNOUNCE a track namespace, register one TrackPublisher per track name
under it, accept inbound SUBSCRIBEs from the peer, and emit
OBJECT_DATAGRAMs that fan out to every attached subscriber. This is what
the speaker / host path needs in nests — phases M6/M7 layer audio
capture + a NestsSpeaker API on top.
Public API additions:
- `MoqSession.announce(namespace, parameters)` — sends ANNOUNCE, awaits
ANNOUNCE_OK, returns an `AnnounceHandle`.
- `AnnounceHandle.openTrack(name)` — registers a `TrackPublisher` for a
track under the announced namespace. Idempotent insert is rejected.
- `TrackPublisher.send(payload)` — encodes one MoQ OBJECT_DATAGRAM and
emits it to every currently-attached subscriber. Group id is fixed at
zero per the audio-rooms NIP draft; object ids are monotonic.
- `TrackPublisher.close()` — sends SUBSCRIBE_DONE to attached
subscribers, removes the track from its parent announce.
- `AnnounceHandle.unannounce()` — sends UNANNOUNCE on the wire, closes
every registered publisher.
- `ErrorCode.TRACK_DOES_NOT_EXIST` and `SubscribeDoneStatus.{TRACK_ENDED,
UNSUBSCRIBED}` constants for the SUBSCRIBE_ERROR / SUBSCRIBE_DONE
values we emit.
Internal additions:
- `pendingAnnounces` keyed by namespace, mirroring the existing
`pendingSubscribes` pattern.
- `announces` map tracking live publishers per namespace.
- `inboundSubscribers` + `publisherSubscribers` so the control-pump can
fan an inbound SUBSCRIBE to the right TrackPublisher and so
TrackPublisher.send can snapshot subscribers without per-OBJECT
locking.
- Control pump now routes ANNOUNCE_OK / ANNOUNCE_ERROR (publisher-side
ack) and inbound SUBSCRIBE / UNSUBSCRIBE (we're being asked to send
OBJECTs, or stop). Unknown-track SUBSCRIBE replies with
SUBSCRIBE_ERROR(TRACK_DOES_NOT_EXIST) so the peer doesn't hang.
- `close()` propagates session death into every AnnounceHandleImpl /
TrackPublisherImpl so any in-flight `send` short-circuits cleanly.
All shared-state mutation is funneled through the existing `stateMutex`
(no `synchronized` — that's JVM-only and would break commonMain). The
session-wide `writeMutex` continues to serialize control-stream writes.
Tests (new in MoqSessionTest):
- announce → ANNOUNCE_OK happy path; UNANNOUNCE wire frame on close.
- ANNOUNCE_ERROR surfaces as MoqProtocolException.
- End-to-end: announce + openTrack + peer SUBSCRIBE → SUBSCRIBE_OK +
three OBJECT_DATAGRAMs round-trip with intact group/object ids.
- send() returns false when no subscribers are attached (no buffering).
- Inbound SUBSCRIBE for unknown track under an announced namespace
replies SUBSCRIBE_ERROR.
Verified: `:nestsClient:jvmTest` (12 tests, 0 failures) + `:quic:jvmTest`
(green) + `./gradlew spotlessApply` clean.