Skip to content

Commit

Permalink
Implement Default Effects (#625)
Browse files Browse the repository at this point in the history
Signed-off-by: mramotar <mramotar@dropbox.com>
  • Loading branch information
matt-ramotar authored Mar 16, 2024
1 parent 5575a0b commit cf30814
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.mobilenativefoundation.paging.core

import kotlinx.coroutines.flow.Flow

/**
* Represents a collector for [PagingSource.LoadResult] objects.
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
* @param A The type of custom actions that can be dispatched.
*/
interface PagingSourceCollector<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any> {
/**
* Collects the load results from the [PagingSource] and dispatches appropriate [PagingAction] objects.
*
* @param params The [PagingSource.LoadParams] associated with the load operation.
* @param results The flow of [PagingSource.LoadResult] instances representing the load results.
* @param state The current [PagingState] when collecting the load results.
* @param dispatch The function to dispatch [PagingAction] instances based on the load results.
*/
suspend operator fun invoke(
params: PagingSource.LoadParams<K, P>,
results: Flow<PagingSource.LoadResult<Id, K, P, D, E>>,
state: PagingState<Id, K, P, D, E>,
dispatch: (action: PagingAction<Id, K, P, D, E, A>) -> Unit
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.mobilenativefoundation.paging.core

import kotlinx.coroutines.flow.Flow

/**
* Represents a provider of [PagingSource.LoadResult] streams.
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
*/
interface PagingSourceStreamProvider<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any> {
/**
* Provides a flow of [PagingSource.LoadResult] instances for the specified [PagingSource.LoadParams].
*
* @param params The [PagingSource.LoadParams] for which to provide the load result stream.
* @return A flow of [PagingSource.LoadResult] instances representing the load results.
*/
fun provide(params: PagingSource.LoadParams<K, P>): Flow<PagingSource.LoadResult<Id, K, P, D, E>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.mobilenativefoundation.paging.core

fun interface StorePagingSourceKeyFactory<Id : Comparable<Id>, K : Any, P : Any, D : Any> {
fun createKeyFor(single: PagingData.Single<Id, K, P, D>): PagingKey<K, P>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.mobilenativefoundation.paging.core

/**
* A type alias for an [Effect] that loads the next page of data when the paging state is [PagingState.Data.Idle].
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
* @param A The type of custom actions that can be dispatched.
*/
typealias LoadNextEffect<Id, K, P, D, E, A> = Effect<Id, K, P, D, E, A, PagingAction.UpdateData<Id, K, P, D, E, A>, PagingState.Data.Idle<Id, K, P, D, E>>

/**
* A type alias for an [Effect] that loads data when a [PagingAction.Load] action is dispatched and the paging state is [PagingState].
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
* @param A The type of custom actions that can be dispatched to modify the paging state.
*/
typealias AppLoadEffect<Id, K, P, D, E, A> = Effect<Id, K, P, D, E, A, PagingAction.Load<Id, K, P, D, E, A>, PagingState<Id, K, P, D, E>>

/**
* A type alias for an [Effect] that loads data when a [PagingAction.User.Load] action is dispatched and the paging state is [PagingState.Loading].
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
* @param A The type of custom actions that can be dispatched to modify the paging state.
*/
typealias UserLoadEffect<Id, K, P, D, E, A> = Effect<Id, K, P, D, E, A, PagingAction.User.Load<Id, K, P, D, E, A>, PagingState.Loading<Id, K, P, D, E>>

/**
* A type alias for an [Effect] that loads more data when a [PagingAction.User.Load] action is dispatched and the paging state is [PagingState.Data.LoadingMore].
*
* @param Id The type of the unique identifier for each item in the paged data.
* @param K The type of the key used for paging.
* @param P The type of the parameters associated with each page of data.
* @param D The type of the data items.
* @param E The type of errors that can occur during the paging process.
* @param A The type of custom actions that can be dispatched to modify the paging state.
*/
typealias UserLoadMoreEffect<Id, K, P, D, E, A> = Effect<Id, K, P, D, E, A, PagingAction.User.Load<Id, K, P, D, E, A>, PagingState.Data.LoadingMore<Id, K, P, D, E>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.AppLoadEffect
import org.mobilenativefoundation.paging.core.Logger
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingSource
import org.mobilenativefoundation.paging.core.PagingSourceCollector
import org.mobilenativefoundation.paging.core.PagingState

class DefaultAppLoadEffect<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
loggerInjector: OptionalInjector<Logger>,
dispatcherInjector: Injector<Dispatcher<Id, K, P, D, E, A>>,
pagingSourceCollectorInjector: Injector<PagingSourceCollector<Id, K, P, D, E, A>>,
pagingSourceInjector: Injector<PagingSource<Id, K, P, D, E>>,
private val jobCoordinator: JobCoordinator,
private val stateManager: StateManager<Id, K, P, D, E>,
) : AppLoadEffect<Id, K, P, D, E, A> {
private val logger = lazy { loggerInjector.inject() }
private val dispatcher = lazy { dispatcherInjector.inject() }
private val pagingSourceCollector = lazy { pagingSourceCollectorInjector.inject() }
private val pagingSource = lazy { pagingSourceInjector.inject() }

override fun invoke(action: PagingAction.Load<Id, K, P, D, E, A>, state: PagingState<Id, K, P, D, E>, dispatch: (PagingAction<Id, K, P, D, E, A>) -> Unit) {
logger.value?.log(
"""Running post reducer effect:
Effect: App load
State: $state
Action: $action
""".trimIndent(),
)

jobCoordinator.launchIfNotActive(action.key) {
val params = PagingSource.LoadParams(action.key, true)
pagingSourceCollector.value(
params,
pagingSource.value.stream(params),
stateManager.state.value,
dispatcher.value::dispatch
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.LoadNextEffect
import org.mobilenativefoundation.paging.core.Logger
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingState

class DefaultLoadNextEffect<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
loggerInjector: OptionalInjector<Logger>,
queueManagerInjector: Injector<QueueManager<K, P>>,
) : LoadNextEffect<Id, K, P, D, E, A> {

private val logger = lazy { loggerInjector.inject() }
private val queueManager = lazy { queueManagerInjector.inject() }

override fun invoke(action: PagingAction.UpdateData<Id, K, P, D, E, A>, state: PagingState.Data.Idle<Id, K, P, D, E>, dispatch: (PagingAction<Id, K, P, D, E, A>) -> Unit) {
logger.value?.log(
"""
Running post reducer effect:
Effect: Load next
State: $state
Action: $action
""".trimIndent(),
)

action.data.collection.nextKey?.key?.let {
queueManager.value.enqueue(action.data.collection.nextKey)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.mobilenativefoundation.paging.core.impl

import kotlinx.coroutines.flow.Flow
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingSource
import org.mobilenativefoundation.paging.core.PagingSourceCollector
import org.mobilenativefoundation.paging.core.PagingState

class DefaultPagingSourceCollector<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any> : PagingSourceCollector<Id, K, P, D, E, A> {
override suspend fun invoke(
params: PagingSource.LoadParams<K, P>,
results: Flow<PagingSource.LoadResult<Id, K, P, D, E>>,
state: PagingState<Id, K, P, D, E>,
dispatch: (action: PagingAction<Id, K, P, D, E, A>) -> Unit
) {
results.collect { result ->
when (result) {
is PagingSource.LoadResult.Data -> dispatch(PagingAction.UpdateData(params, result))
is PagingSource.LoadResult.Error -> dispatch(PagingAction.UpdateError(params, result))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.Logger
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingSource
import org.mobilenativefoundation.paging.core.PagingSourceCollector
import org.mobilenativefoundation.paging.core.PagingState
import org.mobilenativefoundation.paging.core.UserLoadEffect

class DefaultUserLoadEffect<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
loggerInjector: OptionalInjector<Logger>,
dispatcherInjector: Injector<Dispatcher<Id, K, P, D, E, A>>,
pagingSourceCollectorInjector: Injector<PagingSourceCollector<Id, K, P, D, E, A>>,
pagingSourceInjector: Injector<PagingSource<Id, K, P, D, E>>,
private val jobCoordinator: JobCoordinator,
private val stateManager: StateManager<Id, K, P, D, E>,
) : UserLoadEffect<Id, K, P, D, E, A> {
private val logger = lazy { loggerInjector.inject() }
private val dispatcher = lazy { dispatcherInjector.inject() }
private val pagingSourceCollector = lazy { pagingSourceCollectorInjector.inject() }
private val pagingSource = lazy { pagingSourceInjector.inject() }

override fun invoke(action: PagingAction.User.Load<Id, K, P, D, E, A>, state: PagingState.Loading<Id, K, P, D, E>, dispatch: (PagingAction<Id, K, P, D, E, A>) -> Unit) {
logger.value?.log(
"""Running post reducer effect:
Effect: User load
State: $state
Action: $action
""".trimIndent(),
)

jobCoordinator.launch(action.key) {
val params = PagingSource.LoadParams(action.key, true)
pagingSourceCollector.value(
params,
pagingSource.value.stream(params),
stateManager.state.value,
dispatcher.value::dispatch
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.mobilenativefoundation.paging.core.impl

import org.mobilenativefoundation.paging.core.Logger
import org.mobilenativefoundation.paging.core.PagingAction
import org.mobilenativefoundation.paging.core.PagingSource
import org.mobilenativefoundation.paging.core.PagingSourceCollector
import org.mobilenativefoundation.paging.core.PagingState
import org.mobilenativefoundation.paging.core.UserLoadMoreEffect

class DefaultUserLoadMoreEffect<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any, A : Any>(
loggerInjector: OptionalInjector<Logger>,
dispatcherInjector: Injector<Dispatcher<Id, K, P, D, E, A>>,
pagingSourceCollectorInjector: Injector<PagingSourceCollector<Id, K, P, D, E, A>>,
pagingSourceInjector: Injector<PagingSource<Id, K, P, D, E>>,
private val jobCoordinator: JobCoordinator,
private val stateManager: StateManager<Id, K, P, D, E>,
) : UserLoadMoreEffect<Id, K, P, D, E, A> {
private val logger = lazy { loggerInjector.inject() }
private val dispatcher = lazy { dispatcherInjector.inject() }
private val pagingSourceCollector = lazy { pagingSourceCollectorInjector.inject() }
private val pagingSource = lazy { pagingSourceInjector.inject() }

override fun invoke(action: PagingAction.User.Load<Id, K, P, D, E, A>, state: PagingState.Data.LoadingMore<Id, K, P, D, E>, dispatch: (PagingAction<Id, K, P, D, E, A>) -> Unit) {
logger.value?.log(
"""Running post reducer effect:
Effect: User load more
State: $state
Action: $action
""".trimIndent(),
)

jobCoordinator.launch(action.key) {
val params = PagingSource.LoadParams(action.key, true)
pagingSourceCollector.value(
params,
pagingSource.value.stream(params),
stateManager.state.value,
dispatcher.value::dispatch
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.mobilenativefoundation.paging.core.impl

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.mobilenativefoundation.paging.core.PagingData
import org.mobilenativefoundation.paging.core.PagingKey
import org.mobilenativefoundation.paging.core.PagingSource
import org.mobilenativefoundation.paging.core.PagingSourceStreamProvider
import org.mobilenativefoundation.paging.core.StorePagingSourceKeyFactory
import org.mobilenativefoundation.store.store5.StoreReadResponse

class StorePagingSourceStreamProvider<Id : Comparable<Id>, K : Any, P : Any, D : Any, E : Any>(
private val createParentStream: (key: PagingKey<K, P>) -> Flow<PagingSource.LoadResult<Id, K, P, D, E>>,
private val createChildStream: (key: PagingKey<K, P>) -> Flow<StoreReadResponse<PagingData<Id, K, P, D>>>,
private val keyFactory: StorePagingSourceKeyFactory<Id, K, P, D>
) : PagingSourceStreamProvider<Id, K, P, D, E> {
private val pages: MutableMap<PagingKey<K, P>, PagingSource.LoadResult.Data<Id, K, P, D>> = mutableMapOf()
private val mutexForPages = Mutex()

override fun provide(params: PagingSource.LoadParams<K, P>): Flow<PagingSource.LoadResult<Id, K, P, D, E>> =
createParentStream(params.key).map { result ->
when (result) {
is PagingSource.LoadResult.Data -> {
mutexForPages.withLock {
pages[params.key] = result
}

var data = result

result.collection.items.forEach { child ->
val childKey = keyFactory.createKeyFor(child)
initAndCollectChildStream(child, childKey, params.key) { updatedData -> data = updatedData }
}

data
}

is PagingSource.LoadResult.Error -> result
}
}

private fun initAndCollectChildStream(
data: PagingData.Single<Id, K, P, D>,
key: PagingKey<K, P>,
parentKey: PagingKey<K, P>,
emit: (updatedData: PagingSource.LoadResult.Data<Id, K, P, D>) -> Unit
) {
createChildStream(key).distinctUntilChanged().onEach { response ->

if (response is StoreReadResponse.Data) {
val updatedValue = response.value

if (updatedValue is PagingData.Single) {
mutexForPages.withLock {
pages[parentKey]!!.let { currentData ->
val updatedItems = currentData.collection.items.toMutableList()
val indexOfChild = updatedItems.indexOfFirst { it.id == data.id }
val child = updatedItems[indexOfChild]
if (child != updatedValue) {
updatedItems[indexOfChild] = updatedValue

val updatedPage = currentData.copy(collection = currentData.collection.copy(items = updatedItems))

pages[parentKey] = updatedPage

emit(updatedPage)
}
}
}
}
}
}
}
}
Loading

0 comments on commit cf30814

Please sign in to comment.