Skip to content

Commit

Permalink
Remove StoreState and use StoreReadResponse
Browse files Browse the repository at this point in the history
Signed-off-by: mramotar_dbx <mramotar@dropbox.com>
  • Loading branch information
matt-ramotar committed Oct 18, 2023
1 parent 641b2f7 commit 10f8294
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id> = StoreState.Loaded.Collection<Id, StoreData.Single<Id>, StoreData.Collection<Id, StoreData.Single<Id>>>
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 <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> launchStore(
scope: CoroutineScope,
keys: Flow<Key>,
fresh: suspend (currentState: StoreState<Id, Output>, key: Key) -> StoreState<Id, Output>
): StateFlow<StoreState<Id, Output>> {
val stateFlow = MutableStateFlow<StoreState<Id, Output>>(StoreState.Loading)
stream: (key: Key) -> Flow<StoreReadResponse<Output>>,
): StateFlow<StoreReadResponse<Output>> {
val stateFlow = MutableStateFlow<StoreReadResponse<Output>>(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<Output>) {
val joinedDataResponse = joinData(firstKey, stateFlow.value, response)
stateFlow.emit(joinedDataResponse)
} else {
stateFlow.emit(response)
}

if (response is StoreReadResponse.Data<Output> ||
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<Output>
val joinedDataResponse = joinData(key, stateFlow.value, firstDataResponse)
stateFlow.emit(joinedDataResponse)
}
}

Expand All @@ -50,9 +71,9 @@ private fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> launchStore(
fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> Store<Key, Output>.launchStore(
scope: CoroutineScope,
keys: Flow<Key>,
): StateFlow<StoreState<Id, Output>> {
return launchStore(scope, keys) { currentState, key ->
this.freshAndInsertUpdatedItems(currentState, key)
): StateFlow<StoreReadResponse<Output>> {
return launchStore(scope, keys) { key ->
this.stream(StoreReadRequest.fresh(key))
}
}

Expand All @@ -64,72 +85,26 @@ fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> Store<Key, Output>.la
fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> MutableStore<Key, Output>.launchStore(
scope: CoroutineScope,
keys: Flow<Key>,
): StateFlow<StoreState<Id, Output>> {
return launchStore(scope, keys) { currentState, key ->
this.freshAndInsertUpdatedItems(currentState, key)
): StateFlow<StoreReadResponse<Output>> {
return launchStore(scope, keys) { key ->
this.stream<Any>(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 <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> freshAndInsertUpdatedItems(
currentState: StoreState<Id, Output>,
private fun <Id : Any, Key : StoreKey.Collection<Id>, Output : StoreData<Id>> joinData(
key: Key,
get: suspend (key: Key) -> Output,
): StoreState<Id, Output> {
return try {
if (key !is StoreKey.Collection<*>) throw IllegalArgumentException("Invalid key type")

val lastOutput = when (currentState) {
is StoreState.Loaded.Collection<*, *, *> -> (currentState as LoadedCollection<Id>).data
else -> null
}

val nextOutput = get(key) as StoreData.Collection<Id, StoreData.Single<Id>>

val output = (lastOutput?.insertItems(key.loadType, nextOutput.items) ?: nextOutput)
StoreState.Loaded.Collection(output) as StoreState<Id, Output>

} catch (error: Exception) {
StoreState.Error.Exception(error)
prevResponse: StoreReadResponse<Output>,
currentResponse: StoreReadResponse.Data<Output>
): StoreReadResponse.Data<Output> {
val lastOutput = when (prevResponse) {
is StoreReadResponse.Data<Output> -> prevResponse.value as? StoreData.Collection<Id, StoreData.Single<Id>>
else -> null
}
}

/**
* Updates the [Store]'s state based on a provided key.
* @see [freshAndInsertUpdatedItems].
*/
private suspend fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> Store<Key, Output>.freshAndInsertUpdatedItems(
currentState: StoreState<Id, Output>,
key: Key
): StoreState<Id, Output> {
return freshAndInsertUpdatedItems(
currentState,
key
) {
this.fresh(it)
}
}
val currentData = currentResponse.value as StoreData.Collection<Id, StoreData.Single<Id>>

/**
* Updates the [MutableStore]'s state based on a provided key.
* @see [freshAndInsertUpdatedItems].
*/
@OptIn(ExperimentalStoreApi::class)
private suspend fun <Id : Any, Key : StoreKey<Id>, Output : StoreData<Id>> MutableStore<Key, Output>.freshAndInsertUpdatedItems(
currentState: StoreState<Id, Output>,
key: Key
): StoreState<Id, Output> {
return freshAndInsertUpdatedItems(
currentState,
key
) {
this.fresh<Key, Output, Any>(it)
}
val joinedOutput = (lastOutput?.insertItems(key.loadType, currentData.items) ?: currentData) as Output
return StoreReadResponse.Data(joinedOutput, currentResponse.origin)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -37,9 +38,11 @@ class LaunchStoreTests {

stateFlow.test {
val state1 = awaitItem()
assertIs<StoreState.Loading>(state1)
assertIs<StoreReadResponse.Initial>(state1)
val state2 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(state2)
assertIs<StoreReadResponse.Loading>(state2)
val state3 = awaitItem()
assertIs<StoreReadResponse.Data<PostData>>(state3)
expectNoEvents()
}
}
Expand All @@ -53,13 +56,18 @@ class LaunchStoreTests {

stateFlow.test {
val state1 = awaitItem()
assertIs<StoreState.Loading>(state1)
assertIs<StoreReadResponse.Initial>(state1)
val state2 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(state2)

assertIs<StoreReadResponse.Loading>(state2)
val state3 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(state3)
assertEquals(20, state3.data.items.size)
assertIs<StoreReadResponse.Data<PostData>>(state3)
expectNoEvents()

val state4 = awaitItem()
assertIs<StoreReadResponse.Data<PostData>>(state4)
val data4 = state4.value
assertIs<PostData.Feed>(data4)
assertEquals(20, data4.items.size)

expectNoEvents()
}
Expand All @@ -73,10 +81,11 @@ class LaunchStoreTests {

stateFlow.test {
val state1 = awaitItem()
assertIs<StoreState.Loading>(state1)
assertIs<StoreReadResponse.Initial>(state1)
val state2 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(state2)

assertIs<StoreReadResponse.Loading>(state2)
val state3 = awaitItem()
assertIs<StoreReadResponse.Data<PostData>>(state3)
expectNoEvents()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ class PagingTests {

val stateFlow = store.launchStore(this, keys)
stateFlow.test {
val initialState = awaitItem()
assertIs<StoreReadResponse.Initial>(initialState)
val loadingState = awaitItem()
assertIs<StoreState.Loading>(loadingState)
assertIs<StoreReadResponse.Loading>(loadingState)
val loadedState1 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(loadedState1)
assertEquals(10, loadedState1.data.posts.size)
assertIs<StoreReadResponse.Data<PostData.Feed>>(loadedState1)
val data1 = loadedState1.value
assertEquals(10, data1.posts.size)
val loadedState2 = awaitItem()
assertIs<StoreState.Loaded.Collection<String, PostData.Post, PostData.Feed>>(loadedState2)
assertEquals(20, loadedState2.data.posts.size)
assertIs<StoreReadResponse.Data<PostData.Feed>>(loadedState2)
val data2 = loadedState2.value
assertEquals(20, data2.posts.size)
}

val cached = store.stream<PostPutRequestResult>(StoreReadRequest.cached(key1, refresh = false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ sealed class StoreReadResponse<out Output> {
*/
abstract val origin: StoreReadResponseOrigin

object Initial : StoreReadResponse<Nothing>() {
override val origin: StoreReadResponseOrigin = StoreReadResponseOrigin.Initial
}

/**
* Loading event dispatched by [Store] to signal the [Fetcher] is in progress.
*/
Expand Down Expand Up @@ -107,6 +111,7 @@ sealed class StoreReadResponse<out Output> {
is Loading -> this
is NoNewData -> this
is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data")
is Initial -> this
}
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
// for other errors, don't do anything, wait for the read attempt
}

is StoreReadResponse.Initial,
is StoreReadResponse.Loading,
is StoreReadResponse.NoNewData -> {
}
Expand Down

0 comments on commit 10f8294

Please sign in to comment.