package com.suno.android.common_networking.generation import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_networking.ably.AblyEvent import com.suno.android.common_networking.ably.AblyMessage import com.suno.android.common_networking.ably.AblySseClient import com.suno.android.common_networking.remote.common.RemoteClip import com.suno.android.common_networking.remote.session.User import com.suno.android.common_networking.sse.RealtimeEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onCompletion import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton /** * Ably-based implementation of GenerationRealtimeClient using Server-Sent Events. * * Subscribes to the Ably channel `user:{userId}:generate_songs` to receive * real-time song generation status updates. */ @Singleton class AblyGenerationRealtimeClient @Inject constructor( loggerFactory: SunoLogger.Factory, private val ablySseClient: AblySseClient, private val json: Json, ) : GenerationRealtimeClient { private val logger = loggerFactory.create(this@AblyGenerationRealtimeClient) override fun realtimeEventsFlow( userId: Id, ): Flow { val channelName = "user:${userId.value}:generate_songs" logger.d { "Subscribing to generation channel: $channelName" } return ablySseClient.subscribe(channelName) .mapNotNull(::translateAblyEvent) .catch { throwable -> logger.e(throwable) { "Error in generation realtime event stream for user: $userId" } emit(RealtimeEvent.Error(throwable)) } .onCompletion { cause -> when (cause) { null -> logger.d { "Generation realtime subscription completed normally: $userId" } is CancellationException -> logger.d { "Generation realtime subscription cancelled: $userId" } else -> logger.e(cause) { "Generation realtime subscription completed with error" } } } } private fun translateAblyEvent( ablyEvent: AblyEvent, ): GenerationRealtimeEvent? = when (ablyEvent) { is AblyEvent.Connected -> RealtimeEvent.Connected is AblyEvent.Disconnected -> RealtimeEvent.Disconnected is AblyEvent.Message -> parseMessage(ablyEvent.message) is AblyEvent.Error -> RealtimeEvent.Error(ablyEvent.throwable) } private fun parseMessage( message: AblyMessage, ): GenerationRealtimeEvent? { val dataString = message.data ?: run { logger.w { "Ignoring generation message with null data. Full message: $message" } return null } if (dataString.isBlank()) { logger.w { "Ignoring generation message with blank data. Full message: $message" } return null } return runCatching { val update = json.decodeFromString(dataString) logger.d { "Received generation update with ${update.clips.size} clips" } RealtimeEvent.Data(data = update.clips) }.getOrElse { e -> logger.e(e) { "Failed to parse generation update message. Data: '$dataString', Full message: $message" } RealtimeEvent.Error(e) } } } @Serializable private data class GenerationUpdateMessage( @SerialName("clips") val clips: List, )