diff --git a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchStore.kt b/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchStore.kt index 8b7d302e6..1b8d7f647 100644 --- a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchStore.kt +++ b/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/LaunchStore.kt @@ -5,38 +5,59 @@ package org.mobilenativefoundation.store.paging5 import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.mobilenativefoundation.store.store5.ExperimentalStoreApi -import org.mobilenativefoundation.store.store5.MutableStore -import org.mobilenativefoundation.store.store5.Store -import org.mobilenativefoundation.store.store5.impl.extensions.fresh +import org.mobilenativefoundation.store.store5.* - -typealias LoadedCollection = StoreState.Loaded.Collection, StoreData.Collection>> +private class StopProcessingException : Exception() /** * Initializes and returns a [StateFlow] that reflects the state of the Store, updating by a flow of provided keys. * @param scope A [CoroutineScope]. * @param keys A flow of keys that dictate how the Store should be updated. - * @param fresh A lambda that invokes [Store.fresh]. + * @param stream A lambda that invokes [Store.stream]. * @return A read-only [StateFlow] reflecting the state of the Store. */ private fun , Output : StoreData> launchStore( scope: CoroutineScope, keys: Flow, - fresh: suspend (currentState: StoreState, key: Key) -> StoreState -): StateFlow> { - val stateFlow = MutableStateFlow>(StoreState.Loading) + stream: (key: Key) -> Flow>, +): StateFlow> { + val stateFlow = MutableStateFlow>(StoreReadResponse.Initial) + scope.launch { - keys.collect { key -> - val nextState = fresh(stateFlow.value, key) - stateFlow.emit(nextState) + + try { + val firstKey = keys.first() + if (firstKey !is StoreKey.Collection<*>) throw IllegalArgumentException("Invalid key type") + + stream(firstKey).collect { response -> + if (response is StoreReadResponse.Data) { + val joinedDataResponse = joinData(firstKey, stateFlow.value, response) + stateFlow.emit(joinedDataResponse) + } else { + stateFlow.emit(response) + } + + if (response is StoreReadResponse.Data || + response is StoreReadResponse.Error || + response is StoreReadResponse.NoNewData + ) { + throw StopProcessingException() + } + } + + } catch (_: StopProcessingException) { + + } + + keys.drop(1).collect { key -> + if (key !is StoreKey.Collection<*>) throw IllegalArgumentException("Invalid key type") + val firstDataResponse = stream(key).first { it.dataOrNull() != null } as StoreReadResponse.Data + val joinedDataResponse = joinData(key, stateFlow.value, firstDataResponse) + stateFlow.emit(joinedDataResponse) } } @@ -50,9 +71,9 @@ private fun , Output : StoreData> launchStore( fun , Output : StoreData> Store.launchStore( scope: CoroutineScope, keys: Flow, -): StateFlow> { - return launchStore(scope, keys) { currentState, key -> - this.freshAndInsertUpdatedItems(currentState, key) +): StateFlow> { + return launchStore(scope, keys) { key -> + this.stream(StoreReadRequest.fresh(key)) } } @@ -64,72 +85,26 @@ fun , Output : StoreData> Store.la fun , Output : StoreData> MutableStore.launchStore( scope: CoroutineScope, keys: Flow, -): StateFlow> { - return launchStore(scope, keys) { currentState, key -> - this.freshAndInsertUpdatedItems(currentState, key) +): StateFlow> { + return launchStore(scope, keys) { key -> + this.stream(StoreReadRequest.fresh(key)) } } -/** - * Updates the Store's state based on a provided key and a retrieval mechanism. - * @param currentState The current state of the Store. - * @param key The key that dictates how the state should be updated. - * @param get A lambda that defines how to retrieve data from the Store based on a key. - */ -private suspend fun , Output : StoreData> freshAndInsertUpdatedItems( - currentState: StoreState, +private fun , Output : StoreData> joinData( key: Key, - get: suspend (key: Key) -> Output, -): StoreState { - return try { - if (key !is StoreKey.Collection<*>) throw IllegalArgumentException("Invalid key type") - - val lastOutput = when (currentState) { - is StoreState.Loaded.Collection<*, *, *> -> (currentState as LoadedCollection).data - else -> null - } - - val nextOutput = get(key) as StoreData.Collection> - - val output = (lastOutput?.insertItems(key.loadType, nextOutput.items) ?: nextOutput) - StoreState.Loaded.Collection(output) as StoreState - - } catch (error: Exception) { - StoreState.Error.Exception(error) + prevResponse: StoreReadResponse, + currentResponse: StoreReadResponse.Data +): StoreReadResponse.Data { + val lastOutput = when (prevResponse) { + is StoreReadResponse.Data -> prevResponse.value as? StoreData.Collection> + else -> null } -} -/** - * Updates the [Store]'s state based on a provided key. - * @see [freshAndInsertUpdatedItems]. - */ -private suspend fun , Output : StoreData> Store.freshAndInsertUpdatedItems( - currentState: StoreState, - key: Key -): StoreState { - return freshAndInsertUpdatedItems( - currentState, - key - ) { - this.fresh(it) - } -} + val currentData = currentResponse.value as StoreData.Collection> -/** - * Updates the [MutableStore]'s state based on a provided key. - * @see [freshAndInsertUpdatedItems]. - */ -@OptIn(ExperimentalStoreApi::class) -private suspend fun , Output : StoreData> MutableStore.freshAndInsertUpdatedItems( - currentState: StoreState, - key: Key -): StoreState { - return freshAndInsertUpdatedItems( - currentState, - key - ) { - this.fresh(it) - } + val joinedOutput = (lastOutput?.insertItems(key.loadType, currentData.items) ?: currentData) as Output + return StoreReadResponse.Data(joinedOutput, currentResponse.origin) } diff --git a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/StoreState.kt b/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/StoreState.kt deleted file mode 100644 index b8d1943d5..000000000 --- a/paging/src/commonMain/kotlin/org/mobilenativefoundation/store/paging5/StoreState.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.mobilenativefoundation.store.paging5 - -/** - * An interface that defines various states of data-fetching operations. - */ -sealed interface StoreState> { - - /** - * Represents the initial state. - */ - data object Initial : StoreState - - /** - * Represents the loading state. - */ - data object Loading : StoreState - - - /** - * Represents successful fetch operations. - */ - sealed interface Loaded> : StoreState { - - /** - * Represents a successful fetch of an individual item. - */ - data class Single>(val data: Output) : Loaded - - /** - * Represents a successful fetch of a collection of items. - */ - data class Collection, CO : StoreData.Collection>(val data: CO) : - Loaded - } - - /** - * Represents unsuccessful fetch operations. - */ - sealed interface Error : StoreState { - - /** - * Represents an unsuccessful fetch operation due to an exception. - */ - data class Exception(val error: CustomException) : Error - - /** - * Represents an unsuccessful fetch operation due to an error with a message. - */ - data class Message(val error: String) : Error - } -} \ No newline at end of file diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchStoreTests.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchStoreTests.kt index 484016ae6..eb8358e58 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchStoreTests.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/LaunchStoreTests.kt @@ -9,6 +9,7 @@ import org.junit.Test import org.mobilenativefoundation.store.paging5.util.* import org.mobilenativefoundation.store.store5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.MutableStore +import org.mobilenativefoundation.store.store5.StoreReadResponse import kotlin.test.assertEquals import kotlin.test.assertIs @@ -37,9 +38,11 @@ class LaunchStoreTests { stateFlow.test { val state1 = awaitItem() - assertIs(state1) + assertIs(state1) val state2 = awaitItem() - assertIs>(state2) + assertIs(state2) + val state3 = awaitItem() + assertIs>(state3) expectNoEvents() } } @@ -53,13 +56,18 @@ class LaunchStoreTests { stateFlow.test { val state1 = awaitItem() - assertIs(state1) + assertIs(state1) val state2 = awaitItem() - assertIs>(state2) - + assertIs(state2) val state3 = awaitItem() - assertIs>(state3) - assertEquals(20, state3.data.items.size) + assertIs>(state3) + expectNoEvents() + + val state4 = awaitItem() + assertIs>(state4) + val data4 = state4.value + assertIs(data4) + assertEquals(20, data4.items.size) expectNoEvents() } @@ -73,10 +81,11 @@ class LaunchStoreTests { stateFlow.test { val state1 = awaitItem() - assertIs(state1) + assertIs(state1) val state2 = awaitItem() - assertIs>(state2) - + assertIs(state2) + val state3 = awaitItem() + assertIs>(state3) expectNoEvents() } } diff --git a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/PagingTests.kt b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/PagingTests.kt index 1731940cc..7bbf8f56f 100644 --- a/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/PagingTests.kt +++ b/paging/src/commonTest/kotlin/org/mobilenativefoundation/store/paging5/PagingTests.kt @@ -29,14 +29,18 @@ class PagingTests { val stateFlow = store.launchStore(this, keys) stateFlow.test { + val initialState = awaitItem() + assertIs(initialState) val loadingState = awaitItem() - assertIs(loadingState) + assertIs(loadingState) val loadedState1 = awaitItem() - assertIs>(loadedState1) - assertEquals(10, loadedState1.data.posts.size) + assertIs>(loadedState1) + val data1 = loadedState1.value + assertEquals(10, data1.posts.size) val loadedState2 = awaitItem() - assertIs>(loadedState2) - assertEquals(20, loadedState2.data.posts.size) + assertIs>(loadedState2) + val data2 = loadedState2.value + assertEquals(20, data2.posts.size) } val cached = store.stream(StoreReadRequest.cached(key1, refresh = false)) diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt index e71dd93a3..658baa216 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt @@ -28,6 +28,10 @@ sealed class StoreReadResponse { */ abstract val origin: StoreReadResponseOrigin + object Initial : StoreReadResponse() { + override val origin: StoreReadResponseOrigin = StoreReadResponseOrigin.Initial + } + /** * Loading event dispatched by [Store] to signal the [Fetcher] is in progress. */ @@ -107,6 +111,7 @@ sealed class StoreReadResponse { is Loading -> this is NoNewData -> this is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data") + is Initial -> this } } @@ -129,6 +134,8 @@ sealed class StoreReadResponseOrigin { * @property name Unique name to enable differentiation when [org.mobilenativefoundation.store.store5.Fetcher.fallback] exists */ data class Fetcher(val name: String? = null) : StoreReadResponseOrigin() + + object Initial : StoreReadResponseOrigin() } fun StoreReadResponse.Error.doThrow(): Nothing = when (this) { diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt index 2a7c30140..c3a86420f 100644 --- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt +++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt @@ -272,6 +272,7 @@ internal class RealStore( // for other errors, don't do anything, wait for the read attempt } + is StoreReadResponse.Initial, is StoreReadResponse.Loading, is StoreReadResponse.NoNewData -> { }