update events logic
This commit is contained in:
+30
-41
@@ -7,79 +7,68 @@ import io.ktor.http.HttpMethod
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.annotation.Factory
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.domain.Log
|
||||
import ru.shadowsparky.http.domain.TokenStorage
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
|
||||
import kotlin.math.pow
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Factory
|
||||
class RemoteEventListenerFactory(
|
||||
@Single
|
||||
class RemoteEventListenerImpl(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||
private val authTokenCache: TokenStorage,
|
||||
private val json: Json,
|
||||
private val log: Log
|
||||
) {
|
||||
fun create(suffix: String): RemoteEventListener {
|
||||
return RemoteEventListenerImpl(
|
||||
httpClient,
|
||||
serverConfigurationRepository,
|
||||
authTokenCache,
|
||||
json,
|
||||
log,
|
||||
suffix
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RemoteEventListenerImpl(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||
private val authTokenCache: TokenStorage,
|
||||
private val json: Json,
|
||||
private val log: Log,
|
||||
private val suffix: String
|
||||
) : RemoteEventListener {
|
||||
private val eventFlow = flow {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
override val event = flow {
|
||||
val config = serverConfigurationRepository.getServerConfiguration()
|
||||
val token = authTokenCache.token.first() ?: return@flow
|
||||
log.d(TAG, "listener $suffix started")
|
||||
log.d(TAG, "listener started")
|
||||
try {
|
||||
httpClient.webSocket(
|
||||
method = HttpMethod.Get,
|
||||
request = { url(config.asWebSocketStr() + "/$suffix") }
|
||||
request = { url(config.asWebSocketStr() + "/${RemoteEventHandler.ON_EVENT}") }
|
||||
) {
|
||||
outgoing.send(Frame.Text(token.token))
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val text = (incoming.receive() as? Frame.Text)?.readText()
|
||||
if (text != null) {
|
||||
val decoded = json.decodeFromString<RemoteEvent>(text)
|
||||
log.d(TAG, "receive $text $decoded")
|
||||
emit(decoded)
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
try {
|
||||
val decoded = json.decodeFromString<RemoteEvent>(text)
|
||||
log.d(TAG, "receive $text $decoded")
|
||||
emit(decoded)
|
||||
} catch (e: Exception) {
|
||||
log.e(TAG, e, "failed to decode message: $text")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.e(TAG, e, "websocket closed")
|
||||
log.e(TAG, e, "websocket closed with error")
|
||||
throw e
|
||||
} finally {
|
||||
log.d(TAG, "listener $suffix finished")
|
||||
log.d(TAG, "listener finished")
|
||||
}
|
||||
}
|
||||
|
||||
override val event: Flow<RemoteEvent> = eventFlow.retryExponential { true }
|
||||
}.retryExponential { true }.shareIn(scope, SharingStarted.WhileSubscribed(500), 0)
|
||||
|
||||
private fun <T> Flow<T>.retryExponential(
|
||||
maxRetries: Int = Int.MAX_VALUE,
|
||||
@@ -88,7 +77,7 @@ private class RemoteEventListenerImpl(
|
||||
factor: Double = 2.0,
|
||||
shouldRetry: (Throwable) -> Boolean = { true }
|
||||
): Flow<T> = retryWhen { cause, attempt ->
|
||||
log.d(TAG, "error occurred $suffix", cause)
|
||||
log.d(TAG, "error occurred, attempt=$attempt", cause)
|
||||
if (!shouldRetry(cause) || attempt >= maxRetries || cause is CancellationException) {
|
||||
false
|
||||
} else {
|
||||
@@ -96,7 +85,7 @@ private class RemoteEventListenerImpl(
|
||||
.toLong()
|
||||
.coerceAtMost(maxDelay)
|
||||
log.d(TAG, "retry after delay $delayTime", cause)
|
||||
delay(delayTime)
|
||||
delay(delayTime.milliseconds)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -14,7 +14,6 @@ import ru.shadowsparky.vbox.shared.data.apply
|
||||
import ru.shadowsparky.vbox.shared.data.get
|
||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||
import ru.shadowsparky.vbox.shared.di.DbType
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRequest
|
||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedResponse
|
||||
@@ -30,7 +29,7 @@ class RemoteRecentlyWatchedRepository(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||
private val authTokenCache: TokenStorage,
|
||||
@Named(EventType.RECENT) private val remoteEventListener: RemoteEventListener,
|
||||
private val remoteEventListener: RemoteEventListener
|
||||
) : RecentlyWatchedRepository {
|
||||
|
||||
private suspend fun obtainConfig() = serverConfigurationRepository.getServerConfiguration()
|
||||
|
||||
+3
-4
@@ -4,13 +4,12 @@ import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.post
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koin.core.annotation.Named
|
||||
import ru.shadowsparky.vbox.shared.data.apply
|
||||
import ru.shadowsparky.vbox.shared.data.get
|
||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.IsSavedResponse
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
@@ -24,11 +23,11 @@ import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
|
||||
class RemoteSavedMovieRepository(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||
@Named(EventType.SAVED) private val eventListener: RemoteEventListener
|
||||
private val eventListener: RemoteEventListener
|
||||
) : SavedMovieRepository {
|
||||
|
||||
override fun getAll(): Flow<List<VideoDetails>> {
|
||||
return eventListener.event.filter { it is RemoteEvent.OnSaved }
|
||||
return eventListener.event.filterIsInstance<RemoteEvent.OnSaved>()
|
||||
.map { getAllSingle() }
|
||||
.onStart { emit(getAllSingle()) }
|
||||
}
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@ import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.post
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koin.core.annotation.Factory
|
||||
@@ -12,7 +13,7 @@ import ru.shadowsparky.vbox.shared.data.apply
|
||||
import ru.shadowsparky.vbox.shared.data.get
|
||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||
import ru.shadowsparky.vbox.shared.di.DbType
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.domain.SearchDeleteRequest
|
||||
import ru.shadowsparky.vbox.shared.domain.SearchQueryRequest
|
||||
@@ -27,11 +28,11 @@ import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
|
||||
class RemoteSearchRepository(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||
@Named(EventType.SEARCH) private val removeListener: RemoteEventListener
|
||||
private val removeListener: RemoteEventListener
|
||||
) : SearchRepository {
|
||||
|
||||
override fun search(query: String): Flow<List<String>> {
|
||||
return removeListener.event
|
||||
return removeListener.event.filterIsInstance<RemoteEvent.OnSearch>()
|
||||
.map { searchSingle(query) }
|
||||
.onStart { emit(searchSingle(query)) }
|
||||
}
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@ import ru.shadowsparky.vbox.shared.data.apply
|
||||
import ru.shadowsparky.vbox.shared.data.get
|
||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||
import ru.shadowsparky.vbox.shared.di.DbType
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||
@@ -32,7 +31,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.TagResponse
|
||||
class RemoteMovieTagRepository(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||
@Named(EventType.MOVIE_TAG) private val movieTagListener: RemoteEventListener,
|
||||
private val movieTagListener: RemoteEventListener,
|
||||
private val dispatcherProvider: DispatcherProvider
|
||||
) : MovieTagRepository {
|
||||
|
||||
|
||||
+1
-2
@@ -15,7 +15,6 @@ import ru.shadowsparky.vbox.shared.data.apply
|
||||
import ru.shadowsparky.vbox.shared.data.get
|
||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||
import ru.shadowsparky.vbox.shared.di.DbType
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||
@@ -32,7 +31,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
|
||||
class RemoteUserTagRepository(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||
@Named(EventType.USER_TAG) private val userTagListener: RemoteEventListener,
|
||||
private val userTagListener: RemoteEventListener,
|
||||
private val dispatcherProvider: DispatcherProvider
|
||||
) : UserTagRepository {
|
||||
|
||||
|
||||
-46
@@ -1,53 +1,7 @@
|
||||
package ru.shadowsparky.vbox.shared.di
|
||||
|
||||
import org.koin.core.annotation.Factory
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Named
|
||||
import ru.shadowsparky.vbox.shared.data.RemoteEventListenerFactory
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
|
||||
@Module
|
||||
class EventsModule {
|
||||
|
||||
@Factory
|
||||
@Named(EventType.RECENT)
|
||||
fun provideRecentRemoteEventListener(
|
||||
factory: RemoteEventListenerFactory
|
||||
): RemoteEventListener {
|
||||
return factory.create(RemoteEventHandler.RECENTLY_CHANGED)
|
||||
}
|
||||
|
||||
@Factory
|
||||
@Named(EventType.SEARCH)
|
||||
fun provideSearchRemoteEventListener(
|
||||
factory: RemoteEventListenerFactory
|
||||
): RemoteEventListener {
|
||||
return factory.create(RemoteEventHandler.SEARCH_CHANGED)
|
||||
}
|
||||
|
||||
@Factory
|
||||
@Named(EventType.SAVED)
|
||||
fun provideSavedRemoteEventListener(
|
||||
factory: RemoteEventListenerFactory
|
||||
): RemoteEventListener {
|
||||
return factory.create(RemoteEventHandler.SAVED_CHANGED)
|
||||
}
|
||||
|
||||
@Factory
|
||||
@Named(EventType.MOVIE_TAG)
|
||||
fun provideMovieTagRemoteEventListener(
|
||||
factory: RemoteEventListenerFactory
|
||||
): RemoteEventListener {
|
||||
return factory.create(RemoteEventHandler.MOVIE_TAG_CHANGED)
|
||||
}
|
||||
|
||||
@Factory
|
||||
@Named(EventType.USER_TAG)
|
||||
fun provideUserTagRemoteEventListener(
|
||||
factory: RemoteEventListenerFactory
|
||||
): RemoteEventListener {
|
||||
return factory.create(RemoteEventHandler.USER_TAG_CHANGED)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -9,7 +9,6 @@ import ru.shadowsparky.vbox.shared.data.AppDatabaseProvider
|
||||
import ru.shadowsparky.vbox.shared.data.saved.PosterWithoutSuffixSavedMovieRepository
|
||||
import ru.shadowsparky.vbox.shared.data.saved.RemoteSavedMovieRepository
|
||||
import ru.shadowsparky.vbox.shared.data.saved.SqlSavedMovieRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||
@@ -32,7 +31,7 @@ class SavedModule {
|
||||
fun provideSavedRemoteRepository(
|
||||
httpClient: HttpClient,
|
||||
serverConfigurationRepository: ServerConfigurationRepository,
|
||||
@Named(EventType.SAVED) eventListener: RemoteEventListener
|
||||
eventListener: RemoteEventListener
|
||||
): SavedMovieRepository {
|
||||
val impl =
|
||||
RemoteSavedMovieRepository(httpClient, serverConfigurationRepository, eventListener)
|
||||
|
||||
+23
-3
@@ -6,16 +6,20 @@ import com.arkivanov.decompose.childContext
|
||||
import com.arkivanov.decompose.router.stack.childStackWebNavigation
|
||||
import com.arkivanov.decompose.router.webhistory.WebNavigation
|
||||
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
|
||||
import com.arkivanov.essenty.lifecycle.coroutines.repeatOnLifecycle
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.KSerializer
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.http.domain.TokenStorage
|
||||
import ru.shadowsparky.http.domain.hasAccount
|
||||
import ru.shadowsparky.ui.nav.RootComponent
|
||||
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
|
||||
import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider
|
||||
import ru.shadowsparky.vbox.shared.domain.HeathCheck
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@@ -26,6 +30,7 @@ class VBoxRootComponent(
|
||||
private val healthCheck: HeathCheck,
|
||||
val imageLoaderProvider: ImageLoaderProvider,
|
||||
updateComponentFactory: UpdateComponentFactory,
|
||||
private val remoteEventListener: RemoteEventListener,
|
||||
override val serializer: KSerializer<Route> = Route.serializer(),
|
||||
override val koin: Koin = ru.shadowsparky.koin
|
||||
) : RootComponent<Route>(componentContext), WebNavigationOwner {
|
||||
@@ -41,7 +46,20 @@ class VBoxRootComponent(
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch { updateComponent.checkUpdates(false) }
|
||||
scope.launch {
|
||||
repeatOnLifecycle {
|
||||
if (tokenStorage.hasAccount()) {
|
||||
remoteEventListener.event.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
tokenStorage.token.collect {
|
||||
if (it != null) {
|
||||
updateComponent.checkUpdates(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val webNavigation: WebNavigation<*> =
|
||||
@@ -63,7 +81,8 @@ class RootFactory(
|
||||
private val tokenStorage: TokenStorage,
|
||||
private val heathCheck: HeathCheck,
|
||||
private val imageLoaderProvider: ImageLoaderProvider,
|
||||
private val updateComponentFactory: UpdateComponentFactory
|
||||
private val updateComponentFactory: UpdateComponentFactory,
|
||||
private val remoteEventListener: RemoteEventListener
|
||||
) {
|
||||
fun create(
|
||||
context: ComponentContext,
|
||||
@@ -82,7 +101,8 @@ class RootFactory(
|
||||
listOf(Route.Loading(next = initStack.toSet())),
|
||||
heathCheck,
|
||||
imageLoaderProvider,
|
||||
updateComponentFactory
|
||||
updateComponentFactory,
|
||||
remoteEventListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,8 @@
|
||||
package ru.shadowsparky.vbox.shared.presentation.chat
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.annotation.Factory
|
||||
import org.koin.core.annotation.Named
|
||||
import ru.shadowsparky.chat.domain.ChatRepository
|
||||
@@ -8,6 +10,8 @@ import ru.shadowsparky.chat.domain.ChatTextParser
|
||||
import ru.shadowsparky.chat.presentation.ChatComponent
|
||||
import ru.shadowsparky.ui.nav.ComponentFactory
|
||||
import ru.shadowsparky.ui.nav.RouteNavigator
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||
import ru.shadowsparky.vbox.shared.presentation.nav.Child
|
||||
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
||||
import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
|
||||
@@ -16,7 +20,8 @@ import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
|
||||
@Named(RouteIds.CHAT)
|
||||
class ChatComponentFactory(
|
||||
private val chatRepository: ChatRepository,
|
||||
private val textParser: ChatTextParser
|
||||
private val textParser: ChatTextParser,
|
||||
private val remoteEventListener: RemoteEventListener
|
||||
) : ComponentFactory<Child.ChatChild, Route.Chat, Route> {
|
||||
override fun create(
|
||||
route: Route.Chat,
|
||||
@@ -27,7 +32,8 @@ class ChatComponentFactory(
|
||||
child,
|
||||
chatRepository,
|
||||
textParser,
|
||||
onLinkClicked = { root.nav(Route.Videos(it)) }
|
||||
onLinkClicked = { root.nav(Route.Videos(it)) },
|
||||
refreshFlow = remoteEventListener.event.filterIsInstance<RemoteEvent.OnChatUpdate>().map {}
|
||||
)
|
||||
return Child.ChatChild(comp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user