nestsClient: expose moq-lite max_latency on subscribeSpeaker + correct plan framing

The moq-lite Lite-03 spec puts subscriber-side group-staleness control in
max_latency on the SUBSCRIBE frame: 0 = unlimited (relay falls back to its
MAX_GROUP_AGE = 30 s default); a positive value tells the relay to evict
groups older than that in favour of newer ones during transient backpressure.

Plumb it through:

  NestsListener.subscribeSpeaker(pubkey, maxLatencyMs = 0L)
    └─ MoqLiteNestsListener → MoqLiteSession.subscribe(.., maxLatencyMillis)
    └─ DefaultNestsListener (IETF) ignores — no equivalent on that wire
    └─ ReconnectingHandle re-issues across reconnects

Default stays at 0L for back-compat with the JS reference watcher; callers
opt in for low-latency live audio.

Also rewrites nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md
with corrected framing: of the four "upstream issues" the prior draft listed,
only our own missing MAX_STREAMS_UNI emission was a real bug (RFC 9000 §4.6
violation, fixed in d391ae1d). The relay's per-subscriber queue policy,
unbounded FuturesUnordered, no-timeout open_uni().await, and lack of
lagging-consumer detection are spec-permitted architectural decisions in a
gap moq-lite explicitly leaves to implementations — the spec puts staleness
control on the subscriber via max_latency, which we were sending as 0 the
whole time. Reframed as "feature request worth filing upstream", not "bugs".
This commit is contained in:
Claude
2026-05-01 18:30:37 +00:00
parent 9841e3ca2c
commit 897287be5f
7 changed files with 293 additions and 422 deletions
@@ -33,7 +33,10 @@ class NestsListenerCatalogTest {
override val state: StateFlow<NestsListenerState> =
MutableStateFlow<NestsListenerState>(NestsListenerState.Idle).asStateFlow()
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle = throw UnsupportedOperationException("not under test")
override suspend fun subscribeSpeaker(
speakerPubkeyHex: String,
maxLatencyMs: Long,
): SubscribeHandle = throw UnsupportedOperationException("not under test")
// Intentionally does NOT override subscribeCatalog so the
// interface's default body fires.
@@ -107,7 +107,10 @@ class ReconnectingNestsListenerTest {
val subscribeCount: Int get() = _subscribeCount.get()
val unsubscribeCount: Int get() = _unsubscribeCount.get()
override suspend fun subscribeSpeaker(speakerPubkeyHex: String): SubscribeHandle {
override suspend fun subscribeSpeaker(
speakerPubkeyHex: String,
maxLatencyMs: Long,
): SubscribeHandle {
val id = _subscribeCount.incrementAndGet().toLong()
return SubscribeHandle(
subscribeId = id,