package com.suno.android.common_networking.ably import arrow.core.getOrElse import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_networking.remote.orpheus.OrpheusService import com.suno.android.common_networking.sse.SseAuthToken import com.suno.android.common_networking.sse.SseClient import com.suno.android.common_networking.sse.SseConnectionConfig import com.suno.android.common_networking.sse.SseEvent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import java.io.IOException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import javax.inject.Inject import javax.inject.Singleton private const val ABLY_SSE_URL = "https://realtime.ably.io/sse" /** * Client for subscribing to Ably channels via Server-Sent Events (SSE). * * This is an Ably-specific wrapper around the generic SseClient that adds: * - Ably authentication with token refresh * - Ably-specific URL and query parameters * - Ably message parsing */ @Singleton class AblySseClient @Inject constructor( loggerFactory: SunoLogger.Factory, private val sseClient: SseClient, private val messageParser: AblyMessageParser, private val orpheusService: OrpheusService, ) { private val logger = loggerFactory.create(this@AblySseClient) private val _tokenExpirationFlow = MutableStateFlow(value = null) val tokenExpirationFlow: StateFlow = _tokenExpirationFlow.asStateFlow() fun subscribe( channel: String, ): Flow { val config = SseConnectionConfig( url = ABLY_SSE_URL, queryParameters = mapOf( "channels" to channel, "v" to "1.2", ), ) return sseClient.subscribe( config = config, authTokenProvider = { fetchAblyAuthToken() }, ).map(::mapSseEventToAblyEvent) } private suspend fun fetchAblyAuthToken(): Result = runCatching { val authResponse = orpheusService.getAblyAuthToken() .getOrElse { error -> throw IOException("Failed to fetch Ably token: $error") } logger.d { val expiryDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) .format(Date(authResponse.expires)) "Fetched Ably token expiring $expiryDate" } _tokenExpirationFlow.value = authResponse.expires SseAuthToken( authorizationHeader = "Bearer ${authResponse.token}", expiresAtMs = authResponse.expires, ) } private fun mapSseEventToAblyEvent( sseEvent: SseEvent, ): AblyEvent = when (sseEvent) { is SseEvent.Connected -> AblyEvent.Connected is SseEvent.Disconnected -> AblyEvent.Disconnected is SseEvent.Message -> messageParser.parseMessage(sseEvent.data) is SseEvent.Error -> AblyEvent.Error(sseEvent.throwable) } }