diff --git a/src/main/kotlin/chatroom/users/Users.kt b/src/main/kotlin/chatroom/users/Users.kt index 460bbbe..8897018 100644 --- a/src/main/kotlin/chatroom/users/Users.kt +++ b/src/main/kotlin/chatroom/users/Users.kt @@ -8,11 +8,10 @@ val usersModule = module { scope { scoped { UsersPanel(presenter = get()) } - scoped { UsersListUseCase() } scoped { UsersListPresenter( - users = get(), truncate = get(), + repository = get(), scope = get().windowScope, dispatchers = get(), ) diff --git a/src/main/kotlin/chatroom/users/UsersListPresenter.kt b/src/main/kotlin/chatroom/users/UsersListPresenter.kt index c74726c..b96ecdb 100644 --- a/src/main/kotlin/chatroom/users/UsersListPresenter.kt +++ b/src/main/kotlin/chatroom/users/UsersListPresenter.kt @@ -3,29 +3,38 @@ package chatroom.users import arch.Presenter import arch.RokyDispatchers import chatroom.users.UsersViewState.* +import chatserver.ChatRepositories import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class UsersListPresenter( - private val users: UsersListUseCase, private val truncate: UsersListTruncation, + private val repository: ChatRepositories, private val scope: CoroutineScope, dispatchers: RokyDispatchers, ) : Presenter(dispatchers) { + private var job: Job? = null + override fun onAttach(view: UsersListView) { view.show(Empty) - scope.launch(dispatchers.io) { - users() - .truncateUsernames() - .map(::Users).collect { state -> - withContext(dispatchers.main) { - show(state) + + job = + scope.launch(dispatchers.io) { + repository.subscribePresence().subscribe() + repository.readPresence().observe() + .map { it.item.toList() } + .truncateUsernames() + .map(::Users) + .collect { state -> + withContext(dispatchers.main) { + show(state) + } } - } - } + } } private fun show(viewState: UsersViewState) { @@ -33,7 +42,8 @@ class UsersListPresenter( } override fun onDetach(view: UsersListView) { - // Deliberately empty + repository.subscribePresence().unsubscribe() + job?.cancel() } private fun Flow>.truncateUsernames() = diff --git a/src/main/kotlin/chatroom/users/UsersListUseCase.kt b/src/main/kotlin/chatroom/users/UsersListUseCase.kt deleted file mode 100644 index ea9b00e..0000000 --- a/src/main/kotlin/chatroom/users/UsersListUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package chatroom.users - -import chatserver.messages.LocalChatMessages -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlin.random.Random -import kotlin.time.Duration.Companion.seconds - -class UsersListUseCase( - private val users: List = LocalChatMessages.sampleUsers, - private val rndInt: () -> Int = { Random.nextInt(4, users.size - 1) }, - private val rndUser: (List) -> String = { it.random() }, -) { - operator fun invoke(): Flow> = - flow { - while (true) { - val size = rndInt() - val list = (2..size).map { rndUser(users) }.distinct().sorted() - emit(list) - delay(10.seconds) - } - } -} diff --git a/src/main/kotlin/chatserver/ChatRepositories.kt b/src/main/kotlin/chatserver/ChatRepositories.kt index 4d1ab04..06f347d 100644 --- a/src/main/kotlin/chatserver/ChatRepositories.kt +++ b/src/main/kotlin/chatserver/ChatRepositories.kt @@ -1,11 +1,13 @@ package chatserver import chatserver.messages.LocalChatMessages +import chatserver.presence.LocalPresenceRepository import chatserver.profiles.LocalProfilesRepository class ChatRepositories( private val profiles: LocalProfilesRepository, private val messages: LocalChatMessages, + private val presence: LocalPresenceRepository, ) { fun writeProfiles(): WriteChatRepository = profiles @@ -18,4 +20,8 @@ class ChatRepositories( fun subscribeMessages(): SubscribeChatRepository = messages fun writeMessages(): WriteChatRepository = messages + + fun subscribePresence(): SubscribeChatRepository = presence + + fun readPresence(): ReadChatRepository = presence } diff --git a/src/main/kotlin/chatserver/ChatServer.kt b/src/main/kotlin/chatserver/ChatServer.kt index f649021..2dfc874 100644 --- a/src/main/kotlin/chatserver/ChatServer.kt +++ b/src/main/kotlin/chatserver/ChatServer.kt @@ -1,12 +1,13 @@ package chatserver import chatserver.messages.chatServerMessagesModule +import chatserver.presence.chatServerPresenceModule import chatserver.profiles.chatServerProfilesModule import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module val chatServerModule = module { - includes(chatServerProfilesModule, chatServerMessagesModule) + includes(chatServerProfilesModule, chatServerMessagesModule, chatServerPresenceModule) factoryOf(::ChatRepositories) } diff --git a/src/main/kotlin/chatserver/PresenceResult.kt b/src/main/kotlin/chatserver/PresenceResult.kt index 048bf07..819f836 100644 --- a/src/main/kotlin/chatserver/PresenceResult.kt +++ b/src/main/kotlin/chatserver/PresenceResult.kt @@ -4,13 +4,13 @@ import chatserver.ReadChatRepository.ReadResult data class PresenceResult( override val isOk: Boolean, - override val item: Map, + override val item: Set, override val error: Exception?, ) : - ReadResult> { + ReadResult> { companion object { - fun ok(item: Map): PresenceResult = PresenceResult(true, item, null) + fun ok(item: Set): PresenceResult = PresenceResult(true, item, null) - fun fail(e: Exception? = null): PresenceResult = PresenceResult(false, emptyMap(), e) + fun fail(e: Exception? = null): PresenceResult = PresenceResult(false, emptySet(), e) } } diff --git a/src/main/kotlin/chatserver/presence/ChatServerPresence.kt b/src/main/kotlin/chatserver/presence/ChatServerPresence.kt new file mode 100644 index 0000000..816c53a --- /dev/null +++ b/src/main/kotlin/chatserver/presence/ChatServerPresence.kt @@ -0,0 +1,25 @@ +package chatserver.presence + +import arch.RokyDispatchers +import chatserver.messages.LocalChatMessages.Companion.sampleUsers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import org.koin.dsl.module + +val chatServerPresenceModule = + module { + single { + LocalPresenceRepository( + scope = + CoroutineScope( + SupervisorJob() + get().default, + ), + dispatchers = get(), + users = { + with(sampleUsers) { + shuffled().subList(0, (4.. Set, + private val scope: CoroutineScope, + private val dispatchers: RokyDispatchers, +) : SubscribeChatRepository, ReadChatRepository { + private val state: MutableStateFlow = MutableStateFlow(PresenceResult.ok(emptySet())) + private var job: Job? = null + + override fun subscribe() { + unsubscribe() + job = + scope.launch(dispatchers.default) { + while (true) { + state.value = PresenceResult.ok(users()) + delay(10.seconds) + } + } + } + + override fun unsubscribe() { + job?.cancel() + } + + override fun latest(): PresenceResult = state.value + + override fun observe(): Flow = state.asStateFlow() +} diff --git a/src/test/kotlin/chatroom/users/UsersListPresenterTest.kt b/src/test/kotlin/chatroom/users/UsersListPresenterTest.kt index 54c465c..8ee6259 100644 --- a/src/test/kotlin/chatroom/users/UsersListPresenterTest.kt +++ b/src/test/kotlin/chatroom/users/UsersListPresenterTest.kt @@ -3,6 +3,7 @@ package chatroom.users import arch.RokyDispatchers import chatroom.users.UsersViewState.Empty import chatroom.users.UsersViewState.Users +import chatserver.* import coAnswersDelayed import io.mockk.* import kotlinx.coroutines.CoroutineScope @@ -18,22 +19,29 @@ import org.junit.jupiter.api.Test @OptIn(ExperimentalCoroutinesApi::class) class UsersListPresenterTest { private lateinit var scope: CoroutineScope - private lateinit var listUsers: UsersListUseCase + private lateinit var subscribePresence: SubscribeChatRepository + private lateinit var readPresence: ReadChatRepository private lateinit var view: UsersListView private lateinit var presenter: UsersListPresenter @BeforeEach fun setUp() { view = mockk(relaxed = true) - listUsers = mockk(relaxed = true) - coEvery { listUsers() } coAnswersDelayed { flowOf() } + val repositories: ChatRepositories = mockk(relaxed = true) + subscribePresence = mockk() + readPresence = mockk() + every { repositories.subscribePresence() } returns subscribePresence + every { subscribePresence.subscribe() } just runs + every { subscribePresence.unsubscribe() } just runs + every { repositories.readPresence() } returns readPresence + coEvery { readPresence.observe() } coAnswersDelayed { flowOf() } val dispatchers: RokyDispatchers = mockk().apply { every { main } returns dispatcher every { io } returns dispatcher } scope = CoroutineScope(dispatcher) - presenter = UsersListPresenter(listUsers, UsersListTruncation(10), scope, dispatchers) + presenter = UsersListPresenter(UsersListTruncation(10), repositories, scope, dispatchers) } @Test @@ -46,7 +54,7 @@ class UsersListPresenterTest { @Test fun `when users list is empty, then show empty list of users`() = runTest(dispatcher) { - coEvery { listUsers() } coAnswersDelayed { flowOf(emptyList()) } + coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(emptySet())) } presenter.attach(view) advanceUntilIdle() verifyOrder { @@ -63,8 +71,8 @@ class UsersListPresenterTest { @Test fun `when users list is not empty, then show list of users`() = runTest(dispatcher) { - val userList = listOf("Robert", "Tom", "Allie", "Kai") - coEvery { listUsers() } coAnswersDelayed { flowOf(userList) } + val userSet = setOf("Robert", "Tom", "Allie", "Kai") + coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(userSet)) } presenter.attach(view) advanceUntilIdle() verifyOrder { @@ -72,7 +80,7 @@ class UsersListPresenterTest { view.show( withArg { assertTrue { it is Users } - assertTrue { (it as Users).users == userList } + assertTrue { (it as Users).users.toSet() == userSet } }, ) } @@ -81,8 +89,8 @@ class UsersListPresenterTest { @Test fun `when users list contains very long usernames, then truncate long usernames only`() = runTest(dispatcher) { - val userList = listOf("Allie", "Kai", "A very very long username") - coEvery { listUsers() } coAnswersDelayed { flowOf(userList) } + val userSet = setOf("Allie", "Kai", "A very very long username") + coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(userSet)) } presenter.attach(view) advanceUntilIdle() verifyOrder { diff --git a/src/test/kotlin/chatroom/users/UsersListUseCaseTest.kt b/src/test/kotlin/chatroom/users/UsersListUseCaseTest.kt deleted file mode 100644 index 77835f7..0000000 --- a/src/test/kotlin/chatroom/users/UsersListUseCaseTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package chatroom.users - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class UsersListUseCaseTest { - @Test - fun `given list of users, when selecting users five times, then list size is five`() = - runTest { - var index = 0 - val users = listOf("Terry", "Matthew", "Rob", "Tom", "Kai", "Stefano") - val useCase = - UsersListUseCase( - users = users, - rndInt = { 6 }, - rndUser = { it[index++ % it.size] }, - ) - val emissions = mutableListOf>() - val job = - backgroundScope.launch { - useCase().collect(emissions::add) - } - advanceTimeBy(15.seconds) - assertEquals(5, emissions.first().size) - job.cancel() - } - - @Test - fun `given list of users, when selecting users five times, then emit sorted list of five users`() = - runTest { - var index = 0 - val users = listOf("Terry", "Matthew", "Rob", "Tom", "Kai", "Stefano") - val useCase = - UsersListUseCase( - users = users, - rndInt = { 6 }, - rndUser = { it[index++ % it.size] }, - ) - val emissions = mutableListOf>() - val job = - backgroundScope.launch { - useCase().collect(emissions::add) - } - advanceTimeBy(15.seconds) - assertEquals(listOf("Kai", "Matthew", "Rob", "Terry", "Tom"), emissions.first()) - job.cancel() - } -}