Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Default Effects #625

Merged
merged 1 commit into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading