update events logic
This commit is contained in:
+15
-24
@@ -1,7 +1,7 @@
|
|||||||
package ru.shadowsparky.vbox.backend.data
|
package ru.shadowsparky.vbox.backend.data
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Single
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||||
@@ -9,34 +9,25 @@ import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
|||||||
|
|
||||||
val eventLogger: Logger = LoggerFactory.getLogger("events")
|
val eventLogger: Logger = LoggerFactory.getLogger("events")
|
||||||
|
|
||||||
@Factory
|
@Single
|
||||||
class BackendRemoteEventHandler(
|
class BackendRemoteEventHandler(
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
private val sessionCache: SessionCache
|
private val registry: SessionRegistry
|
||||||
) : RemoteEventHandler {
|
) : RemoteEventHandler {
|
||||||
|
|
||||||
suspend fun put(userId: Long, socketSession: SessionCache.Writer) {
|
|
||||||
sessionCache.put(userId, socketSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun remove(userId: Long, socketSession: SessionCache.Writer) {
|
|
||||||
sessionCache.remove(userId, socketSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun notify(eventInfo: RemoteEvent) {
|
override suspend fun notify(eventInfo: RemoteEvent) {
|
||||||
val json = json.encodeToString(eventInfo)
|
val jsonText = json.encodeToString(eventInfo)
|
||||||
val writers = sessionCache.get(eventInfo.userId)
|
val writers = registry.get(eventInfo.userId)
|
||||||
if (writers.isNullOrEmpty()) {
|
if (writers.isNullOrEmpty()) {
|
||||||
eventLogger.info("unable to notify $eventInfo. sessions not found, cache $sessionCache")
|
eventLogger.info("unable to notify {}. sessions not found, cache {}", eventInfo, registry)
|
||||||
} else {
|
return
|
||||||
writers.forEach {
|
}
|
||||||
eventLogger.info("notify[$eventInfo]. session $it", RuntimeException("called"))
|
writers.toList().forEach { writer ->
|
||||||
try {
|
eventLogger.info("notify[{}]. session {}", eventInfo, writer)
|
||||||
it.writeText(json)
|
try {
|
||||||
} catch (_: Exception) {
|
writer.writeText(jsonText)
|
||||||
eventLogger.error("unable to notify $it. delete session")
|
} catch (e: Exception) {
|
||||||
remove(eventInfo.userId, it)
|
eventLogger.error("unable to notify {}. delete session. Reason: {}", writer, e.message)
|
||||||
}
|
registry.remove(eventInfo.userId, writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package ru.shadowsparky.vbox.backend.data
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.koin.core.annotation.Factory
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
typealias SessionMap = MutableMap<Long, List<SessionCache.Writer>>
|
|
||||||
|
|
||||||
@Factory
|
|
||||||
class SessionCache {
|
|
||||||
private val sessionMap: SessionMap = Collections.synchronizedMap(hashMapOf())
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
suspend fun put(userId: Long, socketSession: Writer) {
|
|
||||||
eventLogger.info("put[$userId]=$socketSession")
|
|
||||||
mutex.withLock {
|
|
||||||
sessionMap[userId] = (sessionMap[userId] ?: mutableListOf()) + listOf(socketSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun get(userId: Long): List<Writer>? {
|
|
||||||
mutex.withLock {
|
|
||||||
return sessionMap[userId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun remove(userId: Long, session: Writer) {
|
|
||||||
eventLogger.info("remove[$userId]=$session")
|
|
||||||
mutex.withLock {
|
|
||||||
val sessionFromMap = sessionMap[userId]?.toMutableList()
|
|
||||||
val item = sessionFromMap?.firstOrNull { it == session } ?: return
|
|
||||||
sessionFromMap.remove(item)
|
|
||||||
sessionMap[userId] = sessionFromMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface Writer {
|
|
||||||
suspend fun writeText(text: String)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.shadowsparky.vbox.backend.data
|
||||||
|
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
private typealias SessionMap = ConcurrentHashMap<Long, MutableSet<SessionRegistry.Writer>>
|
||||||
|
|
||||||
|
@Single
|
||||||
|
class SessionRegistry {
|
||||||
|
private val sessionMap: SessionMap = ConcurrentHashMap()
|
||||||
|
|
||||||
|
fun put(userId: Long, socketSession: Writer) {
|
||||||
|
eventLogger.info("put[$userId]=$socketSession")
|
||||||
|
sessionMap.computeIfAbsent(userId) { ConcurrentHashMap.newKeySet() }
|
||||||
|
.add(socketSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(userId: Long): Set<Writer>? {
|
||||||
|
return sessionMap[userId]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(userId: Long, session: Writer) {
|
||||||
|
eventLogger.info("remove[$userId]=$session")
|
||||||
|
sessionMap.computeIfPresent(userId) { _, set ->
|
||||||
|
set.remove(session)
|
||||||
|
if (set.isEmpty()) null else set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Writer {
|
||||||
|
suspend fun writeText(text: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-3
@@ -8,11 +8,9 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.domain.DispatcherProvider
|
import ru.shadowsparky.domain.DispatcherProvider
|
||||||
import ru.shadowsparky.http.domain.BadRequestException
|
import ru.shadowsparky.http.domain.BadRequestException
|
||||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
|
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
|
||||||
@@ -22,7 +20,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
|
|||||||
class MovieTagRepositoryFactory(
|
class MovieTagRepositoryFactory(
|
||||||
private val appDatabase: AppDatabase,
|
private val appDatabase: AppDatabase,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
@Named(EventType.MOVIE_TAG) private val movieEventHandler: RemoteEventHandler
|
private val movieEventHandler: RemoteEventHandler
|
||||||
) {
|
) {
|
||||||
fun create(userId: Long): MovieTagRepository {
|
fun create(userId: Long): MovieTagRepository {
|
||||||
return BackendMovieTagRepository(
|
return BackendMovieTagRepository(
|
||||||
|
|||||||
+1
-3
@@ -5,10 +5,8 @@ import app.cash.sqldelight.coroutines.mapToList
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
|
import ru.shadowsparky.vbox.shared.domain.tag.TagInfo
|
||||||
@@ -18,7 +16,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
|
|||||||
class UserTagRepositoryFactory(
|
class UserTagRepositoryFactory(
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
@Named(EventType.USER_TAG) private val userTagEventHandler: RemoteEventHandler
|
private val userTagEventHandler: RemoteEventHandler
|
||||||
) {
|
) {
|
||||||
fun create(userId: Long): UserTagRepository {
|
fun create(userId: Long): UserTagRepository {
|
||||||
return BackendUserTagRepository(
|
return BackendUserTagRepository(
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package ru.shadowsparky.vbox.backend.di
|
package ru.shadowsparky.vbox.backend.di
|
||||||
|
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import ru.shadowsparky.backend.data.TokenVerifier
|
import ru.shadowsparky.backend.data.TokenVerifier
|
||||||
import ru.shadowsparky.backend.domain.JwtInfo
|
import ru.shadowsparky.backend.domain.JwtInfo
|
||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
|
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
|
||||||
|
import ru.shadowsparky.vbox.backend.data.SessionRegistry
|
||||||
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
|
||||||
@@ -15,7 +14,7 @@ import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory
|
|||||||
import ru.shadowsparky.vbox.backend.di.factory.RecentlyWatchedRepositoryFactory
|
import ru.shadowsparky.vbox.backend.di.factory.RecentlyWatchedRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.di.factory.SavedMovieRepositoryFactory
|
import ru.shadowsparky.vbox.backend.di.factory.SavedMovieRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.di.factory.SearchRepositoryFactory
|
import ru.shadowsparky.vbox.backend.di.factory.SearchRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
import ru.shadowsparky.vbox.shared.domain.VideoApi
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
@@ -28,17 +27,14 @@ class RoutingEntryPoint(
|
|||||||
val userTagFactory: UserTagRepositoryFactory,
|
val userTagFactory: UserTagRepositoryFactory,
|
||||||
val updateFetcherFactory: BackendUpdateFetcherFactory,
|
val updateFetcherFactory: BackendUpdateFetcherFactory,
|
||||||
val chatRepositoryFactory: BackendChatRepositoryFactory,
|
val chatRepositoryFactory: BackendChatRepositoryFactory,
|
||||||
val processUserMessageUseCase: ProcessUserMessageUseCase
|
val processUserMessageUseCase: ProcessUserMessageUseCase,
|
||||||
|
val remoteEventHandler: RemoteEventHandler
|
||||||
)
|
)
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class WebSocketEntryPoint(
|
class WebSocketEntryPoint(
|
||||||
@Named(EventType.SEARCH) val searchEventHandler: BackendRemoteEventHandler,
|
val tokenVerifier: TokenVerifier,
|
||||||
@Named(EventType.RECENT) val recentlyEventHandler: BackendRemoteEventHandler,
|
val sessionRegistry: SessionRegistry
|
||||||
@Named(EventType.SAVED) val savedEventHandler: BackendRemoteEventHandler,
|
|
||||||
@Named(EventType.USER_TAG) val userTagEventHandler: BackendRemoteEventHandler,
|
|
||||||
@Named(EventType.MOVIE_TAG) val movieTagEventHandler: BackendRemoteEventHandler,
|
|
||||||
val tokenVerifier: TokenVerifier
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
|
|||||||
@@ -1,32 +1,9 @@
|
|||||||
package ru.shadowsparky.vbox.backend.di
|
package ru.shadowsparky.vbox.backend.di
|
||||||
|
|
||||||
import org.koin.core.annotation.Module
|
import org.koin.core.annotation.Module
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import org.koin.core.annotation.Single
|
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class EventModule {
|
class EventModule {
|
||||||
|
|
||||||
@Single(binds = [BackendRemoteEventHandler::class])
|
|
||||||
@Named(EventType.SEARCH)
|
|
||||||
fun provideSearch(impl: BackendRemoteEventHandler): RemoteEventHandler = impl
|
|
||||||
|
|
||||||
@Single(binds = [BackendRemoteEventHandler::class])
|
|
||||||
@Named(EventType.RECENT)
|
|
||||||
fun provideRecent(impl: BackendRemoteEventHandler): RemoteEventHandler = impl
|
|
||||||
|
|
||||||
@Single(binds = [BackendRemoteEventHandler::class])
|
|
||||||
@Named(EventType.SAVED)
|
|
||||||
fun provideSaved(impl: BackendRemoteEventHandler): RemoteEventHandler = impl
|
|
||||||
|
|
||||||
@Single(binds = [BackendRemoteEventHandler::class])
|
|
||||||
@Named(EventType.USER_TAG)
|
|
||||||
fun provideUserTag(impl: BackendRemoteEventHandler): RemoteEventHandler = impl
|
|
||||||
|
|
||||||
@Single(binds = [BackendRemoteEventHandler::class])
|
|
||||||
@Named(EventType.MOVIE_TAG)
|
|
||||||
fun provideMovieTag(impl: BackendRemoteEventHandler): RemoteEventHandler = impl
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,18 +1,16 @@
|
|||||||
package ru.shadowsparky.vbox.backend.di.factory
|
package ru.shadowsparky.vbox.backend.di.factory
|
||||||
|
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendRecentlyWatchedRepository
|
import ru.shadowsparky.vbox.backend.data.BackendRecentlyWatchedRepository
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class RecentlyWatchedRepositoryFactory(
|
class RecentlyWatchedRepositoryFactory(
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
@Named(EventType.RECENT) private val remoteEventHandler: RemoteEventHandler
|
private val remoteEventHandler: RemoteEventHandler
|
||||||
) {
|
) {
|
||||||
fun create(userId: Long): BackendRecentlyWatchedRepository {
|
fun create(userId: Long): BackendRecentlyWatchedRepository {
|
||||||
return BackendRecentlyWatchedRepository(db, userId, dispatcherProvider, remoteEventHandler)
|
return BackendRecentlyWatchedRepository(db, userId, dispatcherProvider, remoteEventHandler)
|
||||||
|
|||||||
+1
-3
@@ -1,13 +1,11 @@
|
|||||||
package ru.shadowsparky.vbox.backend.di.factory
|
package ru.shadowsparky.vbox.backend.di.factory
|
||||||
|
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.backend.data.Logger
|
import ru.shadowsparky.backend.data.Logger
|
||||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendSavedMovieRepository
|
import ru.shadowsparky.vbox.backend.data.BackendSavedMovieRepository
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
@@ -15,7 +13,7 @@ class SavedMovieRepositoryFactory(
|
|||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
@Named(EventType.SAVED) private val eventHandler: BackendRemoteEventHandler
|
private val eventHandler: BackendRemoteEventHandler
|
||||||
) {
|
) {
|
||||||
fun create(userId: Long): SavedMovieRepository {
|
fun create(userId: Long): SavedMovieRepository {
|
||||||
return BackendSavedMovieRepository(db, userId, dispatcherProvider, logger, eventHandler)
|
return BackendSavedMovieRepository(db, userId, dispatcherProvider, logger, eventHandler)
|
||||||
|
|||||||
+1
-3
@@ -1,11 +1,9 @@
|
|||||||
package ru.shadowsparky.vbox.backend.di.factory
|
package ru.shadowsparky.vbox.backend.di.factory
|
||||||
|
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendSearchRepository
|
import ru.shadowsparky.vbox.backend.data.BackendSearchRepository
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
import ru.shadowsparky.vbox.shared.domain.SearchRepository
|
import ru.shadowsparky.vbox.shared.domain.SearchRepository
|
||||||
|
|
||||||
@@ -13,7 +11,7 @@ import ru.shadowsparky.vbox.shared.domain.SearchRepository
|
|||||||
class SearchRepositoryFactory(
|
class SearchRepositoryFactory(
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
@Named(EventType.SEARCH) private val eventHandler: RemoteEventHandler
|
private val eventHandler: RemoteEventHandler
|
||||||
) {
|
) {
|
||||||
fun create(userId: Long): SearchRepository {
|
fun create(userId: Long): SearchRepository {
|
||||||
return BackendSearchRepository(db, userId, dispatcherProvider, eventHandler)
|
return BackendSearchRepository(db, userId, dispatcherProvider, eventHandler)
|
||||||
|
|||||||
+31
-42
@@ -2,7 +2,6 @@ package ru.shadowsparky.vbox.backend.presentation
|
|||||||
|
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.routing.Route
|
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import io.ktor.server.websocket.WebSockets
|
import io.ktor.server.websocket.WebSockets
|
||||||
import io.ktor.server.websocket.pingPeriod
|
import io.ktor.server.websocket.pingPeriod
|
||||||
@@ -11,57 +10,47 @@ import io.ktor.server.websocket.webSocket
|
|||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import ru.shadowsparky.backend.data.TokenVerifier
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
import ru.shadowsparky.vbox.backend.data.SessionRegistry
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
|
||||||
import ru.shadowsparky.vbox.backend.data.SessionCache
|
|
||||||
import ru.shadowsparky.vbox.backend.di.WebSocketEntryPoint
|
import ru.shadowsparky.vbox.backend.di.WebSocketEntryPoint
|
||||||
import ru.shadowsparky.vbox.shared.domain.EventType
|
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRepository
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.SearchRepository
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.USER_ID_ARG
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with(socketEntryPoint) {
|
fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with(socketEntryPoint) {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
pingPeriod = (15).seconds
|
pingPeriod = (30).seconds
|
||||||
timeout = (15).seconds
|
timeout = (30).seconds
|
||||||
maxFrameSize = Long.MAX_VALUE
|
maxFrameSize = Long.MAX_VALUE
|
||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
hashMapOf(
|
listOf(
|
||||||
EventType.SEARCH to searchEventHandler,
|
"${SearchRepository.PREFIX}/onChange",
|
||||||
EventType.RECENT to recentlyEventHandler,
|
"${RecentlyWatchedRepository.PREFIX}/onChange",
|
||||||
EventType.SAVED to savedEventHandler,
|
"${SavedMovieRepository.PREFIX}/onChange",
|
||||||
EventType.USER_TAG to userTagEventHandler,
|
"${UserTagRepository.PREFIX}/onChange",
|
||||||
EventType.MOVIE_TAG to movieTagEventHandler
|
"${MovieTagRepository.PREFIX}/onChange"
|
||||||
).forEach { (key, handler) -> webSocket(key, handler, tokenVerifier) }
|
).forEach {
|
||||||
}
|
webSocket(it) { awaitCancellation() }
|
||||||
}
|
}
|
||||||
|
webSocket(RemoteEventHandler.ON_EVENT) {
|
||||||
private fun Route.webSocket(
|
val frame = (incoming.receive() as Frame.Text).readText()
|
||||||
eventType: String,
|
val userId = tokenVerifier.verify(frame).getClaim(USER_ID_ARG).asLong()
|
||||||
handler: BackendRemoteEventHandler,
|
val session = SessionRegistry.Writer { text -> outgoing.trySend(Frame.Text(text)) }
|
||||||
tokenVerifier: TokenVerifier
|
sessionRegistry.put(userId, session)
|
||||||
) {
|
val deferred = CompletableDeferred<Unit?>()
|
||||||
val path = when (eventType) {
|
try {
|
||||||
EventType.SEARCH -> RemoteEventHandler.SEARCH_CHANGED
|
outgoing.invokeOnClose { deferred.complete(null) }
|
||||||
EventType.RECENT -> RemoteEventHandler.RECENTLY_CHANGED
|
deferred.await()
|
||||||
EventType.SAVED -> RemoteEventHandler.SAVED_CHANGED
|
} finally {
|
||||||
EventType.USER_TAG -> RemoteEventHandler.USER_TAG_CHANGED
|
sessionRegistry.remove(userId, session)
|
||||||
EventType.MOVIE_TAG -> RemoteEventHandler.MOVIE_TAG_CHANGED
|
}
|
||||||
else -> error("Unsupported type $eventType")
|
|
||||||
}
|
|
||||||
webSocket(path) {
|
|
||||||
val frame = (incoming.receive() as Frame.Text).readText()
|
|
||||||
val userId = tokenVerifier.verify(frame).getClaim(USER_ID_ARG).asLong()
|
|
||||||
val session = SessionCache.Writer { text -> outgoing.trySend(Frame.Text(text)) }
|
|
||||||
handler.put(userId, session)
|
|
||||||
val deferred = CompletableDeferred<Unit?>()
|
|
||||||
try {
|
|
||||||
outgoing.invokeOnClose { deferred.complete(null) }
|
|
||||||
deferred.await()
|
|
||||||
} finally {
|
|
||||||
handler.remove(userId, session)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -33,7 +33,6 @@ fun Routing.setupAuthMethods(
|
|||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
authenticate(AUTH_JWT_NAME) {
|
authenticate(AUTH_JWT_NAME) {
|
||||||
get("/chech") { call.respond(HttpStatusCode.OK) }
|
|
||||||
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
|
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
|
||||||
post(AuthTokenRepository.CHANGE_PASS_PATH) {
|
post(AuthTokenRepository.CHANGE_PASS_PATH) {
|
||||||
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
|
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
|
||||||
@@ -45,6 +44,6 @@ fun Routing.setupAuthMethods(
|
|||||||
setupSavedMovie(savedMovieFactory)
|
setupSavedMovie(savedMovieFactory)
|
||||||
setupTagsRouting(userTagFactory, movieTagFactory)
|
setupTagsRouting(userTagFactory, movieTagFactory)
|
||||||
setupUpdates(updateFetcherFactory)
|
setupUpdates(updateFetcherFactory)
|
||||||
setupChat(chatRepositoryFactory, processUserMessageUseCase)
|
setupChat(chatRepositoryFactory, processUserMessageUseCase, remoteEventHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -14,10 +14,13 @@ import ru.shadowsparky.http.domain.BadRequestException
|
|||||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
|
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
|
||||||
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||||
|
|
||||||
fun Route.setupChat(
|
fun Route.setupChat(
|
||||||
chatRepositoryFactory: BackendChatRepositoryFactory,
|
chatRepositoryFactory: BackendChatRepositoryFactory,
|
||||||
processUserMessageUseCase: ProcessUserMessageUseCase
|
processUserMessageUseCase: ProcessUserMessageUseCase,
|
||||||
|
remoteEventHandler: RemoteEventHandler
|
||||||
) {
|
) {
|
||||||
get("$DYNAMIC_PREFIX/chat/query") {
|
get("$DYNAMIC_PREFIX/chat/query") {
|
||||||
val repository = chatRepositoryFactory.create(call.obtainUserId())
|
val repository = chatRepositoryFactory.create(call.obtainUserId())
|
||||||
@@ -30,6 +33,8 @@ fun Route.setupChat(
|
|||||||
if (info.role != ChatRoles.USER) throw BadRequestException("invalid role")
|
if (info.role != ChatRoles.USER) throw BadRequestException("invalid role")
|
||||||
val repository = chatRepositoryFactory.create(userId)
|
val repository = chatRepositoryFactory.create(userId)
|
||||||
repository.send(info)
|
repository.send(info)
|
||||||
call.respond(processUserMessageUseCase.execute(userId))
|
val response = processUserMessageUseCase.execute(userId)
|
||||||
|
remoteEventHandler.notify(RemoteEvent.OnChatUpdate(userId))
|
||||||
|
call.respond(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-41
@@ -7,79 +7,68 @@ import io.ktor.http.HttpMethod
|
|||||||
import io.ktor.websocket.Frame
|
import io.ktor.websocket.Frame
|
||||||
import io.ktor.websocket.readText
|
import io.ktor.websocket.readText
|
||||||
import kotlinx.coroutines.CancellationException
|
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.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.serialization.json.Json
|
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.domain.Log
|
||||||
import ru.shadowsparky.http.domain.TokenStorage
|
import ru.shadowsparky.http.domain.TokenStorage
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
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.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||||
import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
|
import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Factory
|
@Single
|
||||||
class RemoteEventListenerFactory(
|
class RemoteEventListenerImpl(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||||
private val authTokenCache: TokenStorage,
|
private val authTokenCache: TokenStorage,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
private val log: Log
|
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 {
|
) : RemoteEventListener {
|
||||||
private val eventFlow = flow {
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
override val event = flow {
|
||||||
val config = serverConfigurationRepository.getServerConfiguration()
|
val config = serverConfigurationRepository.getServerConfiguration()
|
||||||
val token = authTokenCache.token.first() ?: return@flow
|
val token = authTokenCache.token.first() ?: return@flow
|
||||||
log.d(TAG, "listener $suffix started")
|
log.d(TAG, "listener started")
|
||||||
try {
|
try {
|
||||||
httpClient.webSocket(
|
httpClient.webSocket(
|
||||||
method = HttpMethod.Get,
|
method = HttpMethod.Get,
|
||||||
request = { url(config.asWebSocketStr() + "/$suffix") }
|
request = { url(config.asWebSocketStr() + "/${RemoteEventHandler.ON_EVENT}") }
|
||||||
) {
|
) {
|
||||||
outgoing.send(Frame.Text(token.token))
|
outgoing.send(Frame.Text(token.token))
|
||||||
while (currentCoroutineContext().isActive) {
|
for (frame in incoming) {
|
||||||
val text = (incoming.receive() as? Frame.Text)?.readText()
|
if (frame is Frame.Text) {
|
||||||
if (text != null) {
|
val text = frame.readText()
|
||||||
val decoded = json.decodeFromString<RemoteEvent>(text)
|
try {
|
||||||
log.d(TAG, "receive $text $decoded")
|
val decoded = json.decodeFromString<RemoteEvent>(text)
|
||||||
emit(decoded)
|
log.d(TAG, "receive $text $decoded")
|
||||||
|
emit(decoded)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.e(TAG, e, "failed to decode message: $text")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.e(TAG, e, "websocket closed")
|
log.e(TAG, e, "websocket closed with error")
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
log.d(TAG, "listener $suffix finished")
|
log.d(TAG, "listener finished")
|
||||||
}
|
}
|
||||||
}
|
}.retryExponential { true }.shareIn(scope, SharingStarted.WhileSubscribed(500), 0)
|
||||||
|
|
||||||
override val event: Flow<RemoteEvent> = eventFlow.retryExponential { true }
|
|
||||||
|
|
||||||
private fun <T> Flow<T>.retryExponential(
|
private fun <T> Flow<T>.retryExponential(
|
||||||
maxRetries: Int = Int.MAX_VALUE,
|
maxRetries: Int = Int.MAX_VALUE,
|
||||||
@@ -88,7 +77,7 @@ private class RemoteEventListenerImpl(
|
|||||||
factor: Double = 2.0,
|
factor: Double = 2.0,
|
||||||
shouldRetry: (Throwable) -> Boolean = { true }
|
shouldRetry: (Throwable) -> Boolean = { true }
|
||||||
): Flow<T> = retryWhen { cause, attempt ->
|
): 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) {
|
if (!shouldRetry(cause) || attempt >= maxRetries || cause is CancellationException) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
@@ -96,7 +85,7 @@ private class RemoteEventListenerImpl(
|
|||||||
.toLong()
|
.toLong()
|
||||||
.coerceAtMost(maxDelay)
|
.coerceAtMost(maxDelay)
|
||||||
log.d(TAG, "retry after delay $delayTime", cause)
|
log.d(TAG, "retry after delay $delayTime", cause)
|
||||||
delay(delayTime)
|
delay(delayTime.milliseconds)
|
||||||
true
|
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.get
|
||||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||||
import ru.shadowsparky.vbox.shared.di.DbType
|
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.RecentlyWatchedRepository
|
||||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRequest
|
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRequest
|
||||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedResponse
|
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedResponse
|
||||||
@@ -30,7 +29,7 @@ class RemoteRecentlyWatchedRepository(
|
|||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||||
private val authTokenCache: TokenStorage,
|
private val authTokenCache: TokenStorage,
|
||||||
@Named(EventType.RECENT) private val remoteEventListener: RemoteEventListener,
|
private val remoteEventListener: RemoteEventListener
|
||||||
) : RecentlyWatchedRepository {
|
) : RecentlyWatchedRepository {
|
||||||
|
|
||||||
private suspend fun obtainConfig() = serverConfigurationRepository.getServerConfiguration()
|
private suspend fun obtainConfig() = serverConfigurationRepository.getServerConfiguration()
|
||||||
|
|||||||
+3
-4
@@ -4,13 +4,12 @@ import io.ktor.client.HttpClient
|
|||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.vbox.shared.data.apply
|
import ru.shadowsparky.vbox.shared.data.apply
|
||||||
import ru.shadowsparky.vbox.shared.data.get
|
import ru.shadowsparky.vbox.shared.data.get
|
||||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
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.IsSavedResponse
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
import ru.shadowsparky.vbox.shared.domain.RemoteEvent
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||||
@@ -24,11 +23,11 @@ import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
|
|||||||
class RemoteSavedMovieRepository(
|
class RemoteSavedMovieRepository(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationRepository: ServerConfigurationRepository,
|
private val serverConfigurationRepository: ServerConfigurationRepository,
|
||||||
@Named(EventType.SAVED) private val eventListener: RemoteEventListener
|
private val eventListener: RemoteEventListener
|
||||||
) : SavedMovieRepository {
|
) : SavedMovieRepository {
|
||||||
|
|
||||||
override fun getAll(): Flow<List<VideoDetails>> {
|
override fun getAll(): Flow<List<VideoDetails>> {
|
||||||
return eventListener.event.filter { it is RemoteEvent.OnSaved }
|
return eventListener.event.filterIsInstance<RemoteEvent.OnSaved>()
|
||||||
.map { getAllSingle() }
|
.map { getAllSingle() }
|
||||||
.onStart { emit(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.delete
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koin.core.annotation.Factory
|
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.get
|
||||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||||
import ru.shadowsparky.vbox.shared.di.DbType
|
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.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.domain.SearchDeleteRequest
|
import ru.shadowsparky.vbox.shared.domain.SearchDeleteRequest
|
||||||
import ru.shadowsparky.vbox.shared.domain.SearchQueryRequest
|
import ru.shadowsparky.vbox.shared.domain.SearchQueryRequest
|
||||||
@@ -27,11 +28,11 @@ import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
|
|||||||
class RemoteSearchRepository(
|
class RemoteSearchRepository(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||||
@Named(EventType.SEARCH) private val removeListener: RemoteEventListener
|
private val removeListener: RemoteEventListener
|
||||||
) : SearchRepository {
|
) : SearchRepository {
|
||||||
|
|
||||||
override fun search(query: String): Flow<List<String>> {
|
override fun search(query: String): Flow<List<String>> {
|
||||||
return removeListener.event
|
return removeListener.event.filterIsInstance<RemoteEvent.OnSearch>()
|
||||||
.map { searchSingle(query) }
|
.map { searchSingle(query) }
|
||||||
.onStart { emit(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.get
|
||||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||||
import ru.shadowsparky.vbox.shared.di.DbType
|
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.RemoteEvent
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||||
@@ -32,7 +31,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.TagResponse
|
|||||||
class RemoteMovieTagRepository(
|
class RemoteMovieTagRepository(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||||
@Named(EventType.MOVIE_TAG) private val movieTagListener: RemoteEventListener,
|
private val movieTagListener: RemoteEventListener,
|
||||||
private val dispatcherProvider: DispatcherProvider
|
private val dispatcherProvider: DispatcherProvider
|
||||||
) : MovieTagRepository {
|
) : 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.get
|
||||||
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
import ru.shadowsparky.vbox.shared.data.setJsonBody
|
||||||
import ru.shadowsparky.vbox.shared.di.DbType
|
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.RemoteEvent
|
||||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||||
@@ -32,7 +31,7 @@ import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
|
|||||||
class RemoteUserTagRepository(
|
class RemoteUserTagRepository(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val serverConfigurationProvider: ServerConfigurationRepository,
|
private val serverConfigurationProvider: ServerConfigurationRepository,
|
||||||
@Named(EventType.USER_TAG) private val userTagListener: RemoteEventListener,
|
private val userTagListener: RemoteEventListener,
|
||||||
private val dispatcherProvider: DispatcherProvider
|
private val dispatcherProvider: DispatcherProvider
|
||||||
) : UserTagRepository {
|
) : UserTagRepository {
|
||||||
|
|
||||||
|
|||||||
-46
@@ -1,53 +1,7 @@
|
|||||||
package ru.shadowsparky.vbox.shared.di
|
package ru.shadowsparky.vbox.shared.di
|
||||||
|
|
||||||
import org.koin.core.annotation.Factory
|
|
||||||
import org.koin.core.annotation.Module
|
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
|
@Module
|
||||||
class EventsModule {
|
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.PosterWithoutSuffixSavedMovieRepository
|
||||||
import ru.shadowsparky.vbox.shared.data.saved.RemoteSavedMovieRepository
|
import ru.shadowsparky.vbox.shared.data.saved.RemoteSavedMovieRepository
|
||||||
import ru.shadowsparky.vbox.shared.data.saved.SqlSavedMovieRepository
|
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.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
||||||
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
|
||||||
@@ -32,7 +31,7 @@ class SavedModule {
|
|||||||
fun provideSavedRemoteRepository(
|
fun provideSavedRemoteRepository(
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
serverConfigurationRepository: ServerConfigurationRepository,
|
serverConfigurationRepository: ServerConfigurationRepository,
|
||||||
@Named(EventType.SAVED) eventListener: RemoteEventListener
|
eventListener: RemoteEventListener
|
||||||
): SavedMovieRepository {
|
): SavedMovieRepository {
|
||||||
val impl =
|
val impl =
|
||||||
RemoteSavedMovieRepository(httpClient, serverConfigurationRepository, eventListener)
|
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.stack.childStackWebNavigation
|
||||||
import com.arkivanov.decompose.router.webhistory.WebNavigation
|
import com.arkivanov.decompose.router.webhistory.WebNavigation
|
||||||
import com.arkivanov.decompose.router.webhistory.WebNavigationOwner
|
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.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import org.koin.core.Koin
|
import org.koin.core.Koin
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import ru.shadowsparky.http.domain.TokenStorage
|
import ru.shadowsparky.http.domain.TokenStorage
|
||||||
|
import ru.shadowsparky.http.domain.hasAccount
|
||||||
import ru.shadowsparky.ui.nav.RootComponent
|
import ru.shadowsparky.ui.nav.RootComponent
|
||||||
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
|
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider
|
import ru.shadowsparky.vbox.shared.di.factory.ImageLoaderProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.HeathCheck
|
import ru.shadowsparky.vbox.shared.domain.HeathCheck
|
||||||
|
import ru.shadowsparky.vbox.shared.domain.RemoteEventListener
|
||||||
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
||||||
|
|
||||||
@OptIn(ExperimentalDecomposeApi::class)
|
@OptIn(ExperimentalDecomposeApi::class)
|
||||||
@@ -26,6 +30,7 @@ class VBoxRootComponent(
|
|||||||
private val healthCheck: HeathCheck,
|
private val healthCheck: HeathCheck,
|
||||||
val imageLoaderProvider: ImageLoaderProvider,
|
val imageLoaderProvider: ImageLoaderProvider,
|
||||||
updateComponentFactory: UpdateComponentFactory,
|
updateComponentFactory: UpdateComponentFactory,
|
||||||
|
private val remoteEventListener: RemoteEventListener,
|
||||||
override val serializer: KSerializer<Route> = Route.serializer(),
|
override val serializer: KSerializer<Route> = Route.serializer(),
|
||||||
override val koin: Koin = ru.shadowsparky.koin
|
override val koin: Koin = ru.shadowsparky.koin
|
||||||
) : RootComponent<Route>(componentContext), WebNavigationOwner {
|
) : RootComponent<Route>(componentContext), WebNavigationOwner {
|
||||||
@@ -41,7 +46,20 @@ class VBoxRootComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
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<*> =
|
override val webNavigation: WebNavigation<*> =
|
||||||
@@ -63,7 +81,8 @@ class RootFactory(
|
|||||||
private val tokenStorage: TokenStorage,
|
private val tokenStorage: TokenStorage,
|
||||||
private val heathCheck: HeathCheck,
|
private val heathCheck: HeathCheck,
|
||||||
private val imageLoaderProvider: ImageLoaderProvider,
|
private val imageLoaderProvider: ImageLoaderProvider,
|
||||||
private val updateComponentFactory: UpdateComponentFactory
|
private val updateComponentFactory: UpdateComponentFactory,
|
||||||
|
private val remoteEventListener: RemoteEventListener
|
||||||
) {
|
) {
|
||||||
fun create(
|
fun create(
|
||||||
context: ComponentContext,
|
context: ComponentContext,
|
||||||
@@ -82,7 +101,8 @@ class RootFactory(
|
|||||||
listOf(Route.Loading(next = initStack.toSet())),
|
listOf(Route.Loading(next = initStack.toSet())),
|
||||||
heathCheck,
|
heathCheck,
|
||||||
imageLoaderProvider,
|
imageLoaderProvider,
|
||||||
updateComponentFactory
|
updateComponentFactory,
|
||||||
|
remoteEventListener
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,8 @@
|
|||||||
package ru.shadowsparky.vbox.shared.presentation.chat
|
package ru.shadowsparky.vbox.shared.presentation.chat
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext
|
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.Factory
|
||||||
import org.koin.core.annotation.Named
|
import org.koin.core.annotation.Named
|
||||||
import ru.shadowsparky.chat.domain.ChatRepository
|
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.chat.presentation.ChatComponent
|
||||||
import ru.shadowsparky.ui.nav.ComponentFactory
|
import ru.shadowsparky.ui.nav.ComponentFactory
|
||||||
import ru.shadowsparky.ui.nav.RouteNavigator
|
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.Child
|
||||||
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
import ru.shadowsparky.vbox.shared.presentation.nav.Route
|
||||||
import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
|
import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
|
||||||
@@ -16,7 +20,8 @@ import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
|
|||||||
@Named(RouteIds.CHAT)
|
@Named(RouteIds.CHAT)
|
||||||
class ChatComponentFactory(
|
class ChatComponentFactory(
|
||||||
private val chatRepository: ChatRepository,
|
private val chatRepository: ChatRepository,
|
||||||
private val textParser: ChatTextParser
|
private val textParser: ChatTextParser,
|
||||||
|
private val remoteEventListener: RemoteEventListener
|
||||||
) : ComponentFactory<Child.ChatChild, Route.Chat, Route> {
|
) : ComponentFactory<Child.ChatChild, Route.Chat, Route> {
|
||||||
override fun create(
|
override fun create(
|
||||||
route: Route.Chat,
|
route: Route.Chat,
|
||||||
@@ -27,7 +32,8 @@ class ChatComponentFactory(
|
|||||||
child,
|
child,
|
||||||
chatRepository,
|
chatRepository,
|
||||||
textParser,
|
textParser,
|
||||||
onLinkClicked = { root.nav(Route.Videos(it)) }
|
onLinkClicked = { root.nav(Route.Videos(it)) },
|
||||||
|
refreshFlow = remoteEventListener.event.filterIsInstance<RemoteEvent.OnChatUpdate>().map {}
|
||||||
)
|
)
|
||||||
return Child.ChatChild(comp)
|
return Child.ChatChild(comp)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-14
@@ -1,19 +1,14 @@
|
|||||||
package ru.shadowsparky.vbox.shared.domain
|
package ru.shadowsparky.vbox.shared.domain
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
|
|
||||||
import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
|
|
||||||
|
|
||||||
interface RemoteEventHandler {
|
interface RemoteEventHandler {
|
||||||
suspend fun notify(eventInfo: RemoteEvent)
|
suspend fun notify(eventInfo: RemoteEvent)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SEARCH_CHANGED = "${SearchRepository.PREFIX}/onChange"
|
const val ON_EVENT = "$DYNAMIC_PREFIX/onEvent"
|
||||||
const val RECENTLY_CHANGED = "${RecentlyWatchedRepository.PREFIX}/onChange"
|
|
||||||
const val SAVED_CHANGED = "${SavedMovieRepository.PREFIX}/onChange"
|
|
||||||
const val USER_TAG_CHANGED = "${UserTagRepository.PREFIX}/onChange"
|
|
||||||
const val MOVIE_TAG_CHANGED = "${MovieTagRepository.PREFIX}/onChange"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,26 +21,27 @@ sealed interface RemoteEvent {
|
|||||||
val userId: Long
|
val userId: Long
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("OnSearch")
|
||||||
data class OnSearch(val millis: Long, override val userId: Long) : RemoteEvent
|
data class OnSearch(val millis: Long, override val userId: Long) : RemoteEvent
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("OnRecent")
|
||||||
data class OnRecent(val millis: Long, override val userId: Long, val movieId: Long) :
|
data class OnRecent(val millis: Long, override val userId: Long, val movieId: Long) :
|
||||||
RemoteEvent
|
RemoteEvent
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("OnSaved")
|
||||||
data class OnSaved(override val userId: Long, val movieId: Long) : RemoteEvent
|
data class OnSaved(override val userId: Long, val movieId: Long) : RemoteEvent
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("OnUserTag")
|
||||||
data class OnUserTag(override val userId: Long) : RemoteEvent
|
data class OnUserTag(override val userId: Long) : RemoteEvent
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@SerialName("OnMovieTag")
|
||||||
data class OnMovieTag(override val userId: Long, val movieId: Long) : RemoteEvent
|
data class OnMovieTag(override val userId: Long, val movieId: Long) : RemoteEvent
|
||||||
}
|
|
||||||
|
|
||||||
object EventType {
|
@Serializable
|
||||||
const val SEARCH = "search"
|
@SerialName("OnChatUpdate")
|
||||||
const val RECENT = "recent"
|
data class OnChatUpdate(override val userId: Long) : RemoteEvent
|
||||||
const val SAVED = "saved"
|
|
||||||
const val USER_TAG = "user_tag"
|
|
||||||
const val MOVIE_TAG = "movie_tag"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-22
@@ -3,11 +3,14 @@ package ru.shadowsparky.chat.presentation
|
|||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import com.arkivanov.decompose.value.MutableValue
|
import com.arkivanov.decompose.value.MutableValue
|
||||||
import com.arkivanov.decompose.value.Value
|
import com.arkivanov.decompose.value.Value
|
||||||
|
import com.arkivanov.essenty.lifecycle.coroutines.repeatOnLifecycle
|
||||||
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.shadowsparky.chat.domain.ChatRepository
|
import ru.shadowsparky.chat.domain.ChatRepository
|
||||||
import ru.shadowsparky.chat.domain.ChatRoles
|
import ru.shadowsparky.chat.domain.ChatRoles
|
||||||
@@ -31,10 +34,14 @@ class ChatComponent(
|
|||||||
componentContext: ComponentContext,
|
componentContext: ComponentContext,
|
||||||
private val repository: ChatRepository,
|
private val repository: ChatRepository,
|
||||||
private val textParser: ChatTextParser?,
|
private val textParser: ChatTextParser?,
|
||||||
val onLinkClicked: (String) -> Unit
|
val onLinkClicked: (String) -> Unit,
|
||||||
|
val refreshFlow: Flow<Unit>
|
||||||
) : ComponentContext by componentContext {
|
) : ComponentContext by componentContext {
|
||||||
|
private val errorHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
_state.value = ChatState.Error(throwable.message ?: throwable.toString())
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()).also { ctx ->
|
private val scope = CoroutineScope(SupervisorJob() + errorHandler + Dispatchers.Main.immediate).also { ctx ->
|
||||||
lifecycle.doOnDestroy { ctx.cancel() }
|
lifecycle.doOnDestroy { ctx.cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +50,9 @@ class ChatComponent(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
loadInitialMessages()
|
loadInitialMessages()
|
||||||
|
scope.launch {
|
||||||
|
repeatOnLifecycle { refreshFlow.collect { refreshList() } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseMessage(content: String): List<TextToken> {
|
fun parseMessage(content: String): List<TextToken> {
|
||||||
@@ -52,12 +62,8 @@ class ChatComponent(
|
|||||||
fun loadInitialMessages() {
|
fun loadInitialMessages() {
|
||||||
_state.value = ChatState.Loading
|
_state.value = ChatState.Loading
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
val dbList = repository.query(afterId = null)
|
||||||
val dbList = repository.query(afterId = null)
|
_state.value = ChatState.Data(messages = dbList)
|
||||||
_state.value = ChatState.Data(messages = dbList)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_state.value = ChatState.Error(e.message ?: e.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,20 +72,20 @@ class ChatComponent(
|
|||||||
if (content.isBlank() || currentState.isSending) return
|
if (content.isBlank() || currentState.isSending) return
|
||||||
_state.value = currentState.copy(isSending = true, sendError = null)
|
_state.value = currentState.copy(isSending = true, sendError = null)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
repository.send(Message(role = ChatRoles.USER, content = content, timestamp = 0L))
|
||||||
repository.send(Message(role = ChatRoles.USER, content = content, timestamp = 0L))
|
}
|
||||||
val latestMessageId = currentState.messages.lastOrNull()?.id
|
}
|
||||||
val freshMessages = repository.query(afterId = latestMessageId)
|
|
||||||
_state.value = currentState.copy(
|
private fun refreshList() {
|
||||||
messages = currentState.messages + freshMessages,
|
val currentState = _state.value as? ChatState.Data ?: return
|
||||||
isSending = false
|
scope.launch {
|
||||||
)
|
_state.value = currentState.copy(isSending = true)
|
||||||
} catch (e: Exception) {
|
val latestMessageId = currentState.messages.lastOrNull()?.id
|
||||||
_state.value = currentState.copy(
|
val freshMessages = repository.query(afterId = latestMessageId)
|
||||||
isSending = false,
|
_state.value = currentState.copy(
|
||||||
sendError = e.message ?: e.toString()
|
messages = currentState.messages + freshMessages,
|
||||||
)
|
isSending = false
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user