feat(audio-rooms): NestsReconnectPolicy + Reconnecting state (T4 #2 foundation)

Lays the foundation for the moq-lite reconnect-with-backoff path:

  NestsReconnectPolicy — exponential backoff settings.
                         (initial=1s, multiplier=2, max=30s) by
                         default, mirroring kixelated/moq's JS
                         reference (`delay: { initial: 1000,
                         multiplier: 2, max: 30000 }`).
                         maxAttempts defaults to Int.MAX_VALUE
                         since a long-running room should keep
                         trying as long as the user hasn't left;
                         the Composable's onDispose is the cancel
                         signal.
                         NoRetry sentinel for first-shot-or-fail
                         tests / single-room demos.

  delayForAttempt(n)   — 1-indexed; doubles per attempt; clamps at
                         maxDelayMs. n<1 returns 0.
  isExhausted(n)       — n >= maxAttempts (next retry forbidden).
  init { require(...) } — ctor guards reject zero/negative initial,
                          multiplier <= 1, max < initial, attempts < 1.

  NestsListenerState.Reconnecting / NestsSpeakerState.Reconnecting
  — new state variants with (attempt, delayMs). UI consumes via
  the existing NestsListenerState → ConnectionUiState mapper which
  surfaces Reconnecting under OpeningTransport for the v1 chip;
  a future commit can add a dedicated "Attempt N in Mms" UI.

Tests:
  * First attempt = initialDelayMs
  * Doubling via attempt index until maxDelayMs ceiling
  * Non-standard multiplier still respects ceiling
  * Zero/negative attempt → 0 delay
  * isExhausted at the boundary
  * NoRetry exhausts after attempt 1
  * Constructor rejects invalid inputs

The orchestration layer (mint-fresh-JWT → reopen WT → re-issue
SubscribeHandles + the speaker-side equivalent) is the heavier
follow-up. Deliberately split because:
  1. The state + policy can ship and be consumed by callers ready
     to handle Reconnecting today — no behaviour change for those
     who don't.
  2. The full session-resurrection path needs `MutableSharedFlow`
     buffering per SubscribeHandle so app code's `Flow<MoqObject>`
     doesn't notice the swap. Substantial refactor; better as its
     own commit with its own tests.
This commit is contained in:
Claude
2026-04-26 23:05:19 +00:00
parent 440fc61104
commit 2e38aa6c77
5 changed files with 199 additions and 0 deletions
@@ -906,6 +906,15 @@ private fun NestsListenerState.toUiState(previous: ConnectionUiState): Connectio
ConnectionUiState.Connected
}
is NestsListenerState.Reconnecting -> {
// Reconnect is conceptually a "we're connecting again";
// surface it under the same UI bucket as the initial
// OpeningTransport step so the existing chip/spinner
// works without UI changes. A future commit can add a
// dedicated UI state for "Attempt N in Mms".
ConnectionUiState.Connecting(step = ConnectionUiState.Step.OpeningTransport)
}
is NestsListenerState.Failed -> {
ConnectionUiState.Failed(reason)
}