feat(nests): proactive JWT refresh + reconnect for speaker path

Mirror the listener's ReconnectingNestsListener for the publish side.
moq-auth issues 600 s bearer tokens; without proactive refresh, a user
holding the stage past 10 minutes silently drops when the relay tears
down the WebTransport session and stays off the air until they manually
re-tap Talk. The new wrapper recycles the session at 540 s so the relay
never sees an expired token, and re-issues publishing onto each fresh
session with the user's mute intent replayed on the new handle.

VM swap is a one-line change to DefaultNestsSpeakerConnector. The
caller-owned BroadcastHandle is now the wrapper's stable handle that
survives every refresh.

Six unit tests cover happy path, refresh-without-failure-state, mute
replay across recycle, close idempotence, first-attempt-failure
exception propagation, and post-close startBroadcasting guard.

https://claude.ai/code/session_01HXf3zG3F2ev2ASeQju7Y5S
This commit is contained in:
Claude
2026-04-28 03:17:57 +00:00
parent 2027f6ad37
commit d37eb10b8c
3 changed files with 890 additions and 2 deletions
@@ -36,8 +36,8 @@ import com.vitorpamplona.nestsclient.audio.AudioPlayer
import com.vitorpamplona.nestsclient.audio.NestPlayer
import com.vitorpamplona.nestsclient.audio.OpusDecoder
import com.vitorpamplona.nestsclient.audio.OpusEncoder
import com.vitorpamplona.nestsclient.connectNestsSpeaker
import com.vitorpamplona.nestsclient.connectReconnectingNestsListener
import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker
import com.vitorpamplona.nestsclient.moq.SubscribeHandle
import com.vitorpamplona.nestsclient.transport.WebTransportFactory
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
@@ -1132,9 +1132,18 @@ fun interface NestsSpeakerConnector {
): NestsSpeaker
}
/**
* Production speaker factory — wraps each session in
* [connectReconnectingNestsSpeaker] so transport drops auto-retry
* with exponential backoff AND the moq-auth 600 s JWT TTL is
* proactively refreshed (default 540 s recycle window). The
* returned [BroadcastHandle] survives every refresh — the wrapper
* re-issues publishing onto each fresh session and replays the
* user's mute state on the new handle.
*/
private val DefaultNestsSpeakerConnector =
NestsSpeakerConnector { httpClient, transport, scope, room, signer, pubkey, capF, encF ->
connectNestsSpeaker(
connectReconnectingNestsSpeaker(
httpClient = httpClient,
transport = transport,
scope = scope,