diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/config/ConfigStorage.kt b/domain/src/main/java/com/anytypeio/anytype/domain/config/ConfigStorage.kt index 9e39c28ad4..26dc02c223 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/config/ConfigStorage.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/config/ConfigStorage.kt @@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.config import com.anytypeio.anytype.core_models.Config +@Deprecated("Refactoring needed") interface ConfigStorage { @Deprecated("Unsafe method. Use getOrNull() instead") @Throws(IllegalStateException::class) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionContainer.kt index b95ffd2a4e..664a62135b 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionContainer.kt @@ -5,9 +5,9 @@ import com.anytypeio.anytype.core_models.DVSort import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.SubscriptionEvent -import com.anytypeio.anytype.domain.`object`.move import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.`object`.move import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll @@ -39,6 +39,7 @@ class ObjectTypesSubscriptionContainer( beforeId = null, collection = null ) + store.clear() store.merge( types = initial.results.map { ObjectWrapper.Type(it.map) } ) @@ -130,6 +131,7 @@ class ObjectTypesSubscriptionContainer( suspend fun unsubscribe() = withContext(dispatchers.io) { runCatching { + store.clear() repo.cancelObjectSearchSubscription( listOf(SUBSCRIPTION_ID) ) @@ -157,7 +159,15 @@ class ObjectTypesSubscriptionContainer( val objects: List = emptyList(), val dependencies: List = emptyList(), val lastModified: Long = 0L - ) + ) { + companion object { + fun empty() = Index( + objects = emptyList(), + dependencies = emptyList(), + lastModified = 0L + ) + } + } companion object { const val SUBSCRIPTION_ID = "object-type-store-subscription" diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt index 88eec70fef..551663fd99 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/ObjectTypesSubscriptionManager.kt @@ -5,86 +5,55 @@ import com.anytypeio.anytype.core_models.DVFilter import com.anytypeio.anytype.core_models.DVFilterCondition import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.domain.subscriptions.GlobalSubscription import com.anytypeio.anytype.domain.workspace.SpaceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch class ObjectTypesSubscriptionManager ( private val scope: CoroutineScope = GlobalScope, private val container: ObjectTypesSubscriptionContainer, private val spaceManager: SpaceManager -) { +): GlobalSubscription { - private val pipeline = spaceManager.observe().flatMapLatest { config -> - val params = buildParams(config) - container.observe(params) + val pipeline get() = spaceManager.state().flatMapLatest { state -> + when(state) { + is SpaceManager.State.Space.Active -> { + val params = buildParams(state.config) + container.observe(params) + } + is SpaceManager.State.Space.Idle -> { + flow { + emit(ObjectTypesSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.NoSpace -> { + flow { + container.unsubscribe() + emit(ObjectTypesSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.Init -> { + emptyFlow() + } + } } private var job: Job? = null fun onStart() { job?.cancel() - job = scope.launch { pipeline.collect() } + job = scope.launch { + pipeline.collect() + } } - private fun buildParams(config: Config) = - ObjectTypesSubscriptionContainer.Params( - subscription = ObjectTypesSubscriptionContainer.SUBSCRIPTION_ID, - filters = listOf( - DVFilter( - relation = Relations.LAYOUT, - condition = DVFilterCondition.EQUAL, - value = ObjectType.Layout.OBJECT_TYPE.code.toDouble() - ), - DVFilter( - relation = Relations.SPACE_ID, - condition = DVFilterCondition.EQUAL, - value = config.space - ), - DVFilter( - relation = Relations.IS_DELETED, - condition = DVFilterCondition.NOT_EQUAL, - value = true - ), - DVFilter( - relation = Relations.IS_ARCHIVED, - condition = DVFilterCondition.NOT_EQUAL, - value = true - ), - DVFilter( - relation = Relations.UNIQUE_KEY, - condition = DVFilterCondition.NOT_EMPTY - ), - ), - limit = 0, - offset = 0L, - sorts = emptyList(), - sources = emptyList(), - keys = listOf( - Relations.ID, - Relations.NAME, - Relations.IS_HIDDEN, - Relations.IS_DELETED, - Relations.IS_ARCHIVED, - Relations.SMARTBLOCKTYPES, - Relations.LAYOUT, - Relations.DESCRIPTION, - Relations.ICON_EMOJI, - Relations.SOURCE_OBJECT, - Relations.IS_READ_ONLY, - Relations.RECOMMENDED_LAYOUT, - Relations.DEFAULT_TEMPLATE_ID, - Relations.SPACE_ID, - Relations.UNIQUE_KEY, - Relations.RESTRICTIONS - ), - ignoreWorkspace = true - ) - fun onStop() { scope.launch { container.unsubscribe() @@ -92,4 +61,60 @@ class ObjectTypesSubscriptionManager ( job = null } } + + companion object { + fun buildParams(config: Config) = + ObjectTypesSubscriptionContainer.Params( + subscription = ObjectTypesSubscriptionContainer.SUBSCRIPTION_ID, + filters = listOf( + DVFilter( + relation = Relations.LAYOUT, + condition = DVFilterCondition.EQUAL, + value = ObjectType.Layout.OBJECT_TYPE.code.toDouble() + ), + DVFilter( + relation = Relations.SPACE_ID, + condition = DVFilterCondition.EQUAL, + value = config.space + ), + DVFilter( + relation = Relations.IS_DELETED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_ARCHIVED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.UNIQUE_KEY, + condition = DVFilterCondition.NOT_EMPTY + ), + ), + limit = 0, + offset = 0L, + sorts = emptyList(), + sources = emptyList(), + keys = listOf( + Relations.ID, + Relations.NAME, + Relations.IS_HIDDEN, + Relations.IS_DELETED, + Relations.IS_ARCHIVED, + Relations.SMARTBLOCKTYPES, + Relations.LAYOUT, + Relations.DESCRIPTION, + Relations.ICON_EMOJI, + Relations.SOURCE_OBJECT, + Relations.IS_READ_ONLY, + Relations.RECOMMENDED_LAYOUT, + Relations.DEFAULT_TEMPLATE_ID, + Relations.SPACE_ID, + Relations.UNIQUE_KEY, + Relations.RESTRICTIONS + ), + ignoreWorkspace = true + ) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionContainer.kt index 144969e7b1..51b2876a17 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionContainer.kt @@ -5,9 +5,9 @@ import com.anytypeio.anytype.core_models.DVSort import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.SubscriptionEvent -import com.anytypeio.anytype.domain.`object`.move import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.`object`.move import com.anytypeio.anytype.domain.objects.StoreOfRelations import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll @@ -39,6 +39,7 @@ class RelationsSubscriptionContainer( beforeId = null, collection = null ) + store.clear() store.merge( relations = initial.results.map { ObjectWrapper.Relation(it.map) } ) @@ -132,6 +133,7 @@ class RelationsSubscriptionContainer( suspend fun unsubscribe() = withContext(dispatchers.io) { runCatching { + store.clear() repo.cancelObjectSearchSubscription( listOf(SUBSCRIPTION_ID) ) @@ -159,7 +161,15 @@ class RelationsSubscriptionContainer( val objects: List = emptyList(), val dependencies: List = emptyList(), val lastModified: Long = 0L - ) + ) { + companion object { + fun empty() = Index( + objects = emptyList(), + dependencies = emptyList(), + lastModified = 0L + ) + } + } companion object { const val SUBSCRIPTION_ID = "relation-store-subscription" diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionManager.kt index d0833c93ba..4f76009e31 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/search/RelationsSubscriptionManager.kt @@ -6,13 +6,16 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Marketplace import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.domain.subscriptions.GlobalSubscription import com.anytypeio.anytype.domain.workspace.SpaceManager import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch @@ -20,10 +23,34 @@ class RelationsSubscriptionManager @Inject constructor( private val scope: CoroutineScope = GlobalScope, private val container: RelationsSubscriptionContainer, private val spaceManager: SpaceManager -) { - private val pipeline = spaceManager.observe().flatMapLatest { config -> - val params = buildParams(listOf(config.space, config.techSpace, Marketplace.MARKETPLACE_SPACE_ID)) - container.observe(params) +): GlobalSubscription { + val pipeline get() = spaceManager.state().flatMapLatest { state -> + when(state) { + is SpaceManager.State.Space.Active -> { + val params = buildParams( + spaces = listOf( + state.config.space, + state.config.techSpace, + Marketplace.MARKETPLACE_SPACE_ID + ) + ) + container.observe(params) + } + is SpaceManager.State.Space.Idle -> { + flow { + emit(RelationsSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.NoSpace -> { + flow { + container.unsubscribe() + emit(RelationsSubscriptionContainer.Index.empty()) + } + } + is SpaceManager.State.Init -> { + emptyFlow() + } + } } private var job: Job? = null @@ -33,58 +60,6 @@ class RelationsSubscriptionManager @Inject constructor( job = scope.launch { pipeline.collect() } } - private fun buildParams(spaces: List) = RelationsSubscriptionContainer.Params( - subscription = RelationsSubscriptionContainer.SUBSCRIPTION_ID, - filters = listOf( - DVFilter( - relation = Relations.LAYOUT, - condition = DVFilterCondition.EQUAL, - value = ObjectType.Layout.RELATION.code.toDouble() - ), - DVFilter( - relation = Relations.IS_DELETED, - condition = DVFilterCondition.NOT_EQUAL, - value = true - ), - DVFilter( - relation = Relations.IS_ARCHIVED, - condition = DVFilterCondition.NOT_EQUAL, - value = true - ), - DVFilter( - relation = Relations.SPACE_ID, - condition = DVFilterCondition.IN, - value = spaces - ) - ), - limit = 0, - offset = 0L, - sorts = emptyList(), - sources = emptyList(), - keys = listOf( - Relations.ID, - Relations.SPACE_ID, - Relations.TYPE, - Relations.LAYOUT, - Relations.NAME, - Relations.RELATION_FORMAT, - Relations.RELATION_KEY, - Relations.SCOPE, - Relations.IS_READ_ONLY, - Relations.IS_HIDDEN, - Relations.IS_DELETED, - Relations.IS_ARCHIVED, - Relations.IS_FAVORITE, - Relations.RESTRICTIONS, - Relations.MAX_COUNT, - Relations.RELATION_READ_ONLY_VALUE, - Relations.RELATION_DEFAULT_VALUE, - Relations.RELATION_FORMAT_OBJECT_TYPES, - Relations.SOURCE_OBJECT - ), - ignoreWorkspace = true - ) - fun onStop() { scope.launch { container.unsubscribe() @@ -92,4 +67,58 @@ class RelationsSubscriptionManager @Inject constructor( job = null } } + + companion object { + fun buildParams(spaces: List) = RelationsSubscriptionContainer.Params( + subscription = RelationsSubscriptionContainer.SUBSCRIPTION_ID, + filters = listOf( + DVFilter( + relation = Relations.LAYOUT, + condition = DVFilterCondition.EQUAL, + value = ObjectType.Layout.RELATION.code.toDouble() + ), + DVFilter( + relation = Relations.IS_DELETED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.IS_ARCHIVED, + condition = DVFilterCondition.NOT_EQUAL, + value = true + ), + DVFilter( + relation = Relations.SPACE_ID, + condition = DVFilterCondition.IN, + value = spaces + ) + ), + limit = 0, + offset = 0L, + sorts = emptyList(), + sources = emptyList(), + keys = listOf( + Relations.ID, + Relations.SPACE_ID, + Relations.TYPE, + Relations.LAYOUT, + Relations.NAME, + Relations.RELATION_FORMAT, + Relations.RELATION_KEY, + Relations.SCOPE, + Relations.IS_READ_ONLY, + Relations.IS_HIDDEN, + Relations.IS_DELETED, + Relations.IS_ARCHIVED, + Relations.IS_FAVORITE, + Relations.RESTRICTIONS, + Relations.MAX_COUNT, + Relations.RELATION_READ_ONLY_VALUE, + Relations.RELATION_DEFAULT_VALUE, + Relations.RELATION_FORMAT_OBJECT_TYPES, + Relations.SOURCE_OBJECT + ), + ignoreWorkspace = true + ) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscription.kt b/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscription.kt new file mode 100644 index 0000000000..45f016797c --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/subscriptions/GlobalSubscription.kt @@ -0,0 +1,3 @@ +package com.anytypeio.anytype.domain.subscriptions + +interface GlobalSubscription \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/WorkspaceManager.kt b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/WorkspaceManager.kt index b28293df50..e7c065e402 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/WorkspaceManager.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/WorkspaceManager.kt @@ -10,16 +10,23 @@ import com.anytypeio.anytype.domain.debugging.Logger import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +/** + * Maybe convert to AppStateManager, with different states. + */ interface SpaceManager { suspend fun get(): Id suspend fun set(space: Id): Result + fun getConfig(): Config? fun getConfig(space: SpaceId) : Config? fun observe() : Flow + fun state(): Flow + fun clear() class Impl @Inject constructor( @@ -29,14 +36,15 @@ interface SpaceManager { private val logger: Logger ) : SpaceManager { - private val currentSpace = MutableStateFlow(VAULT) + private val currentSpace = MutableStateFlow(NO_SPACE) private val info = mutableMapOf() override suspend fun get(): Id { val curr = currentSpace.value - return curr.ifEmpty { - configStorage.getOrNull()?.space.orEmpty() + if (curr.isEmpty()) { + logger.logWarning("Accessing space manager in no space state") } + return curr } override fun getConfig(): Config? { @@ -44,7 +52,7 @@ interface SpaceManager { return if (curr.isNotEmpty()) { info[curr] } else { - configStorage.getOrNull() + null } } @@ -67,17 +75,23 @@ interface SpaceManager { override fun observe(): Flow { return currentSpace.mapNotNull { space -> if (space.isEmpty()) { - configStorage.getOrNull() + null + } else { + info[space] + } + } + } + + override fun state(): Flow { + return currentSpace.map { space -> + if (space == NO_SPACE) { + State.NoSpace } else { val config = info[space] - if (config != null) - config - else { - val default = configStorage.getOrNull() - if (default != null && default.space == space) - default - else - null + if (config != null) { + State.Space.Active(config) + } else { + State.Space.Idle(SpaceId(space)) } } } @@ -85,11 +99,20 @@ interface SpaceManager { override fun clear() { info.clear() - currentSpace.value = VAULT + currentSpace.value = NO_SPACE } companion object { - const val VAULT = "" + const val NO_SPACE = "" + } + } + + sealed class State { + data object Init: State() + data object NoSpace: State() + sealed class Space: State() { + data class Idle(val space: SpaceId): Space() + data class Active(val config: Config): Space() } } } diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/ObjectTypesSubscriptionContainerTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/ObjectTypesSubscriptionContainerTest.kt new file mode 100644 index 0000000000..9d0a5cb19a --- /dev/null +++ b/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/ObjectTypesSubscriptionContainerTest.kt @@ -0,0 +1,209 @@ +package com.anytypeio.anytype.domain.subscriptions + +import app.cash.turbine.test +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.SearchResult +import com.anytypeio.anytype.core_models.StubConfig +import com.anytypeio.anytype.core_models.StubObjectType +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.common.DefaultCoroutineTestRule +import com.anytypeio.anytype.domain.objects.DefaultStoreOfObjectTypes +import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionContainer +import com.anytypeio.anytype.domain.search.ObjectTypesSubscriptionManager +import com.anytypeio.anytype.domain.search.SubscriptionEventChannel +import com.anytypeio.anytype.domain.workspace.SpaceManager +import kotlin.test.assertEquals +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub + +class ObjectTypesSubscriptionContainerTest { + + @get:Rule + val coroutineTestRule = DefaultCoroutineTestRule() + + private val dispatchers = AppCoroutineDispatchers( + main = coroutineTestRule.dispatcher, + computation = coroutineTestRule.dispatcher, + io = coroutineTestRule.dispatcher + ) + + @Mock lateinit var spaceManager: SpaceManager + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var channel: SubscriptionEventChannel + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun `should populate store with latest results and clear it when unsubscribe`() = runTest { + + // SETUP + + val delay = 300 + + val subscription = ObjectTypesSubscriptionContainer.SUBSCRIPTION_ID + + val store = DefaultStoreOfObjectTypes() + + val defaultSpaceConfig = StubConfig() + + val alternativeSpaceConfig = StubConfig() + + val container = ObjectTypesSubscriptionContainer( + repo = repo, + channel = channel, + store = store, + dispatchers = dispatchers + ) + + val manager = ObjectTypesSubscriptionManager( + scope = TestScope(), + spaceManager = spaceManager, + container = container + ) + + val defaultSpaceSearchParams = ObjectTypesSubscriptionManager.buildParams( + defaultSpaceConfig + ) + + val alternativeSpaceSearchParams = ObjectTypesSubscriptionManager.buildParams( + alternativeSpaceConfig + ) + + val defaultSpaceTypes = buildList { + add( + StubObjectType() + ) + add( + StubObjectType() + ) + } + + val alternativeSpaceTypes = buildList { + add( + StubObjectType() + ) + add( + StubObjectType() + ) + } + + // STUBBING + + channel.stub { + on { + subscribe( + listOf(subscription) + ) + } doReturn emptyFlow() + } + + repo.stub { + onBlocking { + cancelObjectSearchSubscription(listOf(subscription)) + } doReturn Unit + } + + spaceManager.stub { + on { + state() + } doReturn flow { + emit( + SpaceManager.State.Space.Active( + defaultSpaceConfig + ) + ) + delay(300) + emit( + SpaceManager.State.Space.Active( + alternativeSpaceConfig + ) + ) + } + } + + repo.stub { + onBlocking { + searchObjectsWithSubscription( + subscription = defaultSpaceSearchParams.subscription, + sorts = defaultSpaceSearchParams.sorts, + filters = defaultSpaceSearchParams.filters, + limit = defaultSpaceSearchParams.limit, + offset = defaultSpaceSearchParams.offset, + keys = defaultSpaceSearchParams.keys, + ignoreWorkspace = defaultSpaceSearchParams.ignoreWorkspace, + source = defaultSpaceSearchParams.sources, + afterId = null, + beforeId = null, + collection = null, + noDepSubscription = true + ) + } doReturn SearchResult( + results = defaultSpaceTypes.map { ObjectWrapper.Basic(it.map) }, + dependencies = emptyList() + ) + } + + repo.stub { + onBlocking { + searchObjectsWithSubscription( + subscription = alternativeSpaceSearchParams.subscription, + sorts = alternativeSpaceSearchParams.sorts, + filters = alternativeSpaceSearchParams.filters, + limit = alternativeSpaceSearchParams.limit, + offset = alternativeSpaceSearchParams.offset, + keys = alternativeSpaceSearchParams.keys, + ignoreWorkspace = alternativeSpaceSearchParams.ignoreWorkspace, + source = alternativeSpaceSearchParams.sources, + afterId = null, + beforeId = null, + collection = null, + noDepSubscription = true + ) + } doReturn SearchResult( + results = alternativeSpaceTypes.map { ObjectWrapper.Basic(it.map) }, + dependencies = emptyList() + ) + } + + // TESTING + + manager.pipeline.test { + val first = awaitItem() + assertEquals( + expected = defaultSpaceTypes, + actual = store.getAll() + ) + val second = awaitItem() + awaitComplete() + assertEquals( + expected = alternativeSpaceTypes, + actual = store.getAll() + ) + + container.unsubscribe() + + assertEquals( + expected = emptyList(), + actual = store.getAll() + ) + } + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/RelationsSubscriptionContainerTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/RelationsSubscriptionContainerTest.kt new file mode 100644 index 0000000000..a34cbbab89 --- /dev/null +++ b/domain/src/test/java/com/anytypeio/anytype/domain/subscriptions/RelationsSubscriptionContainerTest.kt @@ -0,0 +1,218 @@ +package com.anytypeio.anytype.domain.subscriptions + +import app.cash.turbine.test +import com.anytypeio.anytype.core_models.Marketplace +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.SearchResult +import com.anytypeio.anytype.core_models.StubConfig +import com.anytypeio.anytype.core_models.StubRelationObject +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import com.anytypeio.anytype.domain.common.DefaultCoroutineTestRule +import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations +import com.anytypeio.anytype.domain.search.RelationsSubscriptionContainer +import com.anytypeio.anytype.domain.search.RelationsSubscriptionManager +import com.anytypeio.anytype.domain.search.SubscriptionEventChannel +import com.anytypeio.anytype.domain.workspace.SpaceManager +import kotlin.test.assertEquals +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub + +class RelationsSubscriptionContainerTest { + + @get:Rule + val coroutineTestRule = DefaultCoroutineTestRule() + + private val dispatchers = AppCoroutineDispatchers( + main = coroutineTestRule.dispatcher, + computation = coroutineTestRule.dispatcher, + io = coroutineTestRule.dispatcher + ) + + @Mock lateinit var spaceManager: SpaceManager + + @Mock + lateinit var repo: BlockRepository + + @Mock + lateinit var channel: SubscriptionEventChannel + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun `should populate store with latest results and clear it when unsubscribe`() = runTest { + + // SETUP + + val delay = 300L + + val subscription = RelationsSubscriptionContainer.SUBSCRIPTION_ID + + val store = DefaultStoreOfRelations() + + val defaultSpaceConfig = StubConfig() + + val alternativeSpaceConfig = StubConfig() + + val container = RelationsSubscriptionContainer( + repo = repo, + channel = channel, + store = store, + dispatchers = dispatchers + ) + + val manager = RelationsSubscriptionManager ( + scope = TestScope(), + spaceManager = spaceManager, + container = container + ) + + val defaultSpaceSearchParams = RelationsSubscriptionManager.buildParams( + spaces = listOf( + defaultSpaceConfig.space, + defaultSpaceConfig.techSpace, + Marketplace.MARKETPLACE_SPACE_ID, + ) + ) + + val alternativeSpaceSearchParams = RelationsSubscriptionManager.buildParams( + spaces = listOf( + alternativeSpaceConfig.space, + alternativeSpaceConfig.techSpace, + Marketplace.MARKETPLACE_SPACE_ID, + ) + ) + + val defaultSpaceTypes = buildList { + add( + StubRelationObject() + ) + add( + StubRelationObject() + ) + } + + val alternativeSpaceTypes = buildList { + add( + StubRelationObject() + ) + add( + StubRelationObject() + ) + } + + // STUBBING + + channel.stub { + on { + subscribe( + listOf(subscription) + ) + } doReturn emptyFlow() + } + + repo.stub { + onBlocking { + cancelObjectSearchSubscription(listOf(subscription)) + } doReturn Unit + } + + spaceManager.stub { + on { + state() + } doReturn flow { + emit( + SpaceManager.State.Space.Active( + defaultSpaceConfig + ) + ) + delay(delay) + emit( + SpaceManager.State.Space.Active( + alternativeSpaceConfig + ) + ) + } + } + + repo.stub { + onBlocking { + searchObjectsWithSubscription( + subscription = defaultSpaceSearchParams.subscription, + sorts = defaultSpaceSearchParams.sorts, + filters = defaultSpaceSearchParams.filters, + limit = defaultSpaceSearchParams.limit, + offset = defaultSpaceSearchParams.offset, + keys = defaultSpaceSearchParams.keys, + ignoreWorkspace = defaultSpaceSearchParams.ignoreWorkspace, + source = defaultSpaceSearchParams.sources, + afterId = null, + beforeId = null, + collection = null, + noDepSubscription = true + ) + } doReturn SearchResult( + results = defaultSpaceTypes.map { ObjectWrapper.Basic(it.map) }, + dependencies = emptyList() + ) + } + + repo.stub { + onBlocking { + searchObjectsWithSubscription( + subscription = alternativeSpaceSearchParams.subscription, + sorts = alternativeSpaceSearchParams.sorts, + filters = alternativeSpaceSearchParams.filters, + limit = alternativeSpaceSearchParams.limit, + offset = alternativeSpaceSearchParams.offset, + keys = alternativeSpaceSearchParams.keys, + ignoreWorkspace = alternativeSpaceSearchParams.ignoreWorkspace, + source = alternativeSpaceSearchParams.sources, + afterId = null, + beforeId = null, + collection = null, + noDepSubscription = true + ) + } doReturn SearchResult( + results = alternativeSpaceTypes.map { ObjectWrapper.Basic(it.map) }, + dependencies = emptyList() + ) + } + + // TESTING + + manager.pipeline.test { + val first = awaitItem() + assertEquals( + expected = defaultSpaceTypes, + actual = store.getAll() + ) + val second = awaitItem() + awaitComplete() + assertEquals( + expected = alternativeSpaceTypes, + actual = store.getAll() + ) + + container.unsubscribe() + + assertEquals( + expected = emptyList(), + actual = store.getAll() + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index 2d3166bb66..911440453e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -1643,6 +1643,7 @@ class HomeScreenViewModel( fun onVaultClicked() { viewModelScope.launch { + spaceManager.clear() clearLastOpenedSpace.async(Unit).fold( onSuccess = { Timber.d("Cleared last opened space before opening vault") diff --git a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt index 40bc439bfe..2b16bf3bfd 100644 --- a/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/test/core-models-stub/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -213,7 +213,7 @@ fun StubCallout( fun StubRelation( relationKey: String = MockDataFactory.randomString(), - format: RelationFormat + format: RelationFormat = Relation.Format.SHORT_TEXT ): Relation = Relation( key = relationKey, name = MockDataFactory.randomString(),