Skip to content

Commit

Permalink
Merge pull request #7 from matt-ramotar/operation-pipeline
Browse files Browse the repository at this point in the history
Redesign operation pipeline
  • Loading branch information
matt-ramotar authored Jul 21, 2024
2 parents 204fd3b + 59590a7 commit b83ca75
Show file tree
Hide file tree
Showing 26 changed files with 209 additions and 269 deletions.
64 changes: 32 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ class TimelineScreenPresenter(

### Adding Sorting and Filtering

1. Implement the `Operation` interface for customized sorting and filtering:
1. Implement the `Operation` abstract class for customized sorting and filtering:

```kotlin
class SortForTimeRange(private val timeRange: TimeRange) :
Operation<Cursor, TimelineRequest, Post> {
override fun shouldApply(
Operation<Cursor, TimelineRequest, Post>() {
internal override fun shouldApply(
key: TimelineRequest?,
pagingState: PagingState<Cursor>,
fetchingState: FetchingState<Cursor, TimelineRequest>
Expand All @@ -134,7 +134,7 @@ class SortForTimeRange(private val timeRange: TimeRange) :
return true
}

override fun apply(
internal override fun apply(
snapshot: ItemSnapshotList<Cursor, Post>,
key: TimelineRequest?,
pagingState: PagingState<Cursor>,
Expand Down Expand Up @@ -181,7 +181,7 @@ class TimelineScreenPresenter(...) : Presenter<TimelineScreen.State> {
val operation = when (sortingMethod) {
is Top -> SortForTimeRange(operation.timeRange)
}
operationPipeline.clear().add(operation)
dispatcher.dispatch(Action.UpdateOperations(operation))
}

return TimelineScreen.State(
Expand Down Expand Up @@ -240,41 +240,41 @@ fun TimelinePostLoadedUi(

object CursorComparator : Comparator<Cursor> {

override fun compare(a: Cursor, b: Cursor): Int {
val parsedCursorA = parseCursor(a)
val parsedCursorB = parseCursor(b)
return parsedCursorA.first.compareTo(parsedCursorB.first)
}
override fun compare(a: Cursor, b: Cursor): Int {
val parsedCursorA = parseCursor(a)
val parsedCursorB = parseCursor(b)
return parsedCursorA.first.compareTo(parsedCursorB.first)
}

override fun distance(a: Cursor, b: Cursor): Int {
val parsedCursorA = parseCursor(a)
val parsedCursorB = parseCursor(b)
override fun distance(a: Cursor, b: Cursor): Int {
val parsedCursorA = parseCursor(a)
val parsedCursorB = parseCursor(b)

// Compare timestamps
val timeDiff = parsedCursorA.first - parsedCursorB.first
// Compare timestamps
val timeDiff = parsedCursorA.first - parsedCursorB.first

return when {
// If timestamps are different, use their difference
// Coercing to Int range to avoid overflow issues
timeDiff != 0L -> timeDiff.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
return when {
// If timestamps are different, use their difference
// Coercing to Int range to avoid overflow issues
timeDiff != 0L -> timeDiff.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()

// If timestamps are the same, compare the unique parts lexicographically
// This ensures a consistent, deterministic ordering
else -> parsedCursorA.second.compareTo(parsedCursorB.second)
// If timestamps are the same, compare the unique parts lexicographically
// This ensures a consistent, deterministic ordering
else -> parsedCursorA.second.compareTo(parsedCursorB.second)
}
}
}

private fun parseCursor(cursor: Cursor): Pair<Long, String> {
// Parsing the cursor string into its components
val parts = cursor.split('-')
require(parts.size == 2) { "Invalid cursor format. Expected 'timestamp-uniqueId'" }
private fun parseCursor(cursor: Cursor): Pair<Long, String> {
// Parsing the cursor string into its components
val parts = cursor.split('-')
require(parts.size == 2) { "Invalid cursor format. Expected 'timestamp-uniqueId'" }

// Converting the timestamp string to a Long for numerical comparison
val timestamp = parts[0].toLongOrNull() ?: throw IllegalArgumentException("Invalid timestamp in cursor")
val uniqueId = parts[1]
// Converting the timestamp string to a Long for numerical comparison
val timestamp = parts[0].toLongOrNull() ?: throw IllegalArgumentException("Invalid timestamp in cursor")
val uniqueId = parts[1]

return timestamp to uniqueId
}
return timestamp to uniqueId
}
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.mobilenativefoundation.storex.paging.runtime.PagingState
* @param ItemId The type of the item identifier.
* @param PageRequestKey The type of the paging key.
*/
interface FetchingStrategy<ItemId : Any, PageRequestKey : Any> {
interface FetchingStrategy<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {

/**
* Determines whether to fetch more data in the forward direction based on the current state of the pager.
Expand All @@ -29,7 +29,7 @@ interface FetchingStrategy<ItemId : Any, PageRequestKey : Any> {
*/
fun shouldFetchForward(
params: PagingSource.LoadParams<PageRequestKey>,
pagingState: PagingState<ItemId>,
pagingState: PagingState<ItemId, PageRequestKey, ItemValue>,
fetchingState: FetchingState<ItemId, PageRequestKey>,
): Boolean

Expand All @@ -43,7 +43,7 @@ interface FetchingStrategy<ItemId : Any, PageRequestKey : Any> {
*/
fun shouldFetchBackward(
params: PagingSource.LoadParams<PageRequestKey>,
pagingState: PagingState<ItemId>,
pagingState: PagingState<ItemId, PageRequestKey, ItemValue>,
fetchingState: FetchingState<ItemId, PageRequestKey>,
): Boolean
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
package org.mobilenativefoundation.storex.paging.runtime

sealed class Action<out PageRequestKey : Any> {
import org.mobilenativefoundation.storex.paging.runtime.internal.pager.api.MutableOperationPipeline

sealed class Action<out ItemId: Any, out PageRequestKey : Any, out ItemValue: Any> {

data class ProcessQueue internal constructor(
val direction: LoadDirection
) : Action<Nothing>()
) : Action<Nothing, Nothing, Nothing>()

data class SkipQueue<K : Any> internal constructor(
val key: K,
val direction: LoadDirection,
val strategy: LoadStrategy,
) : Action<K>()
) : Action<Nothing, K, Nothing>()

data class Enqueue<K : Any> internal constructor(
val key: K,
val direction: LoadDirection,
val strategy: LoadStrategy,
val jump: Boolean
) : Action<K>()
) : Action<Nothing, K, Nothing>()

data object Invalidate : Action<Nothing, Nothing, Nothing>()

data class AddOperation<ItemId: Any, PageRequestKey: Any, ItemValue: Any>(
val operation: Operation<ItemId, PageRequestKey, ItemValue>
) : Action<ItemId, PageRequestKey, ItemValue>()

data class RemoveOperation<ItemId: Any, PageRequestKey: Any, ItemValue: Any>(
val operation: Operation<ItemId, PageRequestKey, ItemValue>
) : Action<ItemId, PageRequestKey, ItemValue>()

data object Invalidate : Action<Nothing>()
data object ClearOperations : Action<Nothing, Nothing, Nothing>()

companion object {
fun <K : Any> skipQueue(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package org.mobilenativefoundation.storex.paging.runtime

interface Dispatcher<PageRequestKey: Any> {
suspend fun dispatch(action: Action<PageRequestKey>)
interface Dispatcher<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {
suspend fun dispatch(action: Action<ItemId, PageRequestKey, ItemValue>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.mobilenativefoundation.storex.paging.runtime

@RequiresOptIn(message = "This API is experimental. It may be changed in the future without notice.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalPagingApi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package org.mobilenativefoundation.storex.paging.runtime
* @param PageRequestKey The type of the paging key.
* @param ItemValue The type of the item value.
*/
interface Operation<ItemId: Any, PageRequestKey: Any, ItemValue: Any> {
abstract class Operation<ItemId: Any, PageRequestKey: Any, ItemValue: Any> {
/**
* Determines whether this operation should be applied based on the current state.
*
Expand All @@ -16,7 +16,7 @@ interface Operation<ItemId: Any, PageRequestKey: Any, ItemValue: Any> {
* @param fetchingState The current fetching state.
* @return True if the operation should be applied, false otherwise.
*/
fun shouldApply(key: PageRequestKey?, pagingState: PagingState<ItemId>, fetchingState: FetchingState<ItemId, PageRequestKey>): Boolean
internal abstract fun shouldApply(key: PageRequestKey?, pagingState: PagingState<ItemId, PageRequestKey, ItemValue>, fetchingState: FetchingState<ItemId, PageRequestKey>): Boolean

/**
* Applies the operation to the given snapshot.
Expand All @@ -27,10 +27,10 @@ interface Operation<ItemId: Any, PageRequestKey: Any, ItemValue: Any> {
* @param fetchingState The current fetching state.
* @return The transformed snapshot after applying the operation.
*/
fun apply(
internal abstract fun apply(
snapshot: ItemSnapshotList<ItemId, ItemValue>,
key: PageRequestKey?,
pagingState: PagingState<ItemId>,
pagingState: PagingState<ItemId, PageRequestKey, ItemValue>,
fetchingState: FetchingState<ItemId, PageRequestKey>
): ItemSnapshotList<ItemId, ItemValue>
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.mobilenativefoundation.storex.paging.runtime

interface OperationPipeline<ItemId: Any, PageRequestKey: Any, ItemValue: Any>: List<Operation<ItemId, PageRequestKey, ItemValue>>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package org.mobilenativefoundation.storex.paging.runtime

import kotlinx.coroutines.flow.StateFlow

interface Pager<ItemId : Any> {
val state: StateFlow<PagingState<ItemId>>
interface Pager<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {
val state: StateFlow<PagingState<ItemId, PageRequestKey, ItemValue>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,34 @@ import org.mobilenativefoundation.storex.paging.persistence.api.ItemPersistence
import org.mobilenativefoundation.storex.paging.persistence.api.PagePersistence
import org.mobilenativefoundation.storex.paging.runtime.internal.pagingScope.impl.PagingScopeBuilder

interface PagingScope<ItemId: Any, PageRequestKey: Any, ItemValue: Any> {
fun getPager(): Pager<ItemId>
fun getOperationManager(): OperationManager<ItemId, PageRequestKey, ItemValue>
fun getDispatcher(): Dispatcher<PageRequestKey>
interface PagingScope<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {
fun getPager(): Pager<ItemId, PageRequestKey, ItemValue>
fun getDispatcher(): Dispatcher<ItemId, PageRequestKey, ItemValue>
fun getUpdatingItemProvider(): UpdatingItemProvider<ItemId, ItemValue>

interface Builder<Id: Any, K: Any, V: Any> {
fun setInitialState(state: PagingState<Id>): Builder<Id, K, V>
fun setInitialLoadParams(params: PagingSource.LoadParams<K>): Builder<Id, K, V>
fun setPagingSource(source: PagingSource<Id, K, V>): Builder<Id, K, V>
fun setCoroutineDispatcher(dispatcher: CoroutineDispatcher): Builder<Id, K, V>
fun addLaunchEffect(effect: LaunchEffect): Builder<Id, K, V>
fun addSideEffect(effect: SideEffect<Id, V>): Builder<Id, K, V>
fun addMiddleware(mw: Middleware<K>): Builder<Id, K, V>
fun setInitialFetchingState(state: FetchingState<Id, K>): Builder<Id, K, V>
fun setFetchingStrategy(strategy: FetchingStrategy<Id, K>): Builder<Id, K, V>
fun setErrorHandlingStrategy(strategy: ErrorHandlingStrategy): Builder<Id, K, V>
fun setItemMemoryCache(cache: MutableMap<Id, V>): Builder<Id, K, V>
fun setPageMemoryCache(cache: MutableMap<K, PagingSource.LoadResult.Data<Id, K, V>>): Builder<Id, K, V>
fun setItemPersistence(persistence: ItemPersistence<Id, K, V>): Builder<Id, K, V>
fun setPagePersistence(persistence: PagePersistence<Id, K, V>): Builder<Id, K, V>
fun setItemUpdater(updater: Updater<Id, V, *>): Builder<Id, K, V>
fun setPlaceholderFactory(placeholderFactory: PlaceholderFactory<Id, K, V>): Builder<Id, K, V>
fun build(): PagingScope<Id, K, V>
interface Builder<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {
fun setInitialState(state: PagingState<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setInitialLoadParams(params: PagingSource.LoadParams<PageRequestKey>): Builder<ItemId, PageRequestKey, ItemValue>
fun setPagingSource(source: PagingSource<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setCoroutineDispatcher(dispatcher: CoroutineDispatcher): Builder<ItemId, PageRequestKey, ItemValue>
fun addLaunchEffect(effect: LaunchEffect): Builder<ItemId, PageRequestKey, ItemValue>
fun addSideEffect(effect: SideEffect<ItemId, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun addMiddleware(mw: Middleware<PageRequestKey>): Builder<ItemId, PageRequestKey, ItemValue>
fun setInitialFetchingState(state: FetchingState<ItemId, PageRequestKey>): Builder<ItemId, PageRequestKey, ItemValue>
fun setFetchingStrategy(strategy: FetchingStrategy<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setErrorHandlingStrategy(strategy: ErrorHandlingStrategy): Builder<ItemId, PageRequestKey, ItemValue>
fun setItemMemoryCache(cache: MutableMap<ItemId, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setPageMemoryCache(cache: MutableMap<PageRequestKey, PagingSource.LoadResult.Data<ItemId, PageRequestKey, ItemValue>>): Builder<ItemId, PageRequestKey, ItemValue>
fun setItemPersistence(persistence: ItemPersistence<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setPagePersistence(persistence: PagePersistence<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setItemUpdater(updater: Updater<ItemId, ItemValue, *>): Builder<ItemId, PageRequestKey, ItemValue>
fun setPlaceholderFactory(placeholderFactory: PlaceholderFactory<ItemId, PageRequestKey, ItemValue>): Builder<ItemId, PageRequestKey, ItemValue>
fun setInitialOperations(operations: List<Operation<ItemId, PageRequestKey, ItemValue>>): Builder<ItemId, PageRequestKey, ItemValue>
fun build(): PagingScope<ItemId, PageRequestKey, ItemValue>
}

companion object {
fun <Id: Any, K: Any, V: Any> builder(
fun <Id : Any, K : Any, V : Any> builder(
pagingConfig: PagingConfig<Id, K>
): Builder<Id, K, V> = PagingScopeBuilder(pagingConfig)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package org.mobilenativefoundation.storex.paging.runtime

import org.mobilenativefoundation.storex.paging.runtime.internal.pager.api.MutableOperationPipeline

data class PagingState<ItemId : Any>(

data class PagingState<ItemId : Any, PageRequestKey : Any, ItemValue : Any>(
val ids: List<ItemId?>,
val loadStates: CombinedLoadStates,
val mutableOperationPipeline: MutableOperationPipeline<ItemId, PageRequestKey, ItemValue>
) {
companion object {
fun <ItemId: Any> initial() = PagingState<ItemId>(
fun <ItemId : Any, PageRequestKey : Any, ItemValue : Any> initial() = PagingState<ItemId, PageRequestKey, ItemValue>(
emptyList(),
CombinedLoadStates.initial()
CombinedLoadStates.initial(),
MutableOperationPipeline.empty()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.mobilenativefoundation.storex.paging.runtime.internal.pager.api

import org.mobilenativefoundation.storex.paging.runtime.Operation
import org.mobilenativefoundation.storex.paging.runtime.OperationPipeline
import org.mobilenativefoundation.storex.paging.runtime.internal.pager.impl.RealMutableOperationPipeline

interface MutableOperationPipeline<ItemId : Any, PageRequestKey : Any, ItemValue : Any> :
OperationPipeline<ItemId, PageRequestKey, ItemValue>, MutableList<Operation<ItemId, PageRequestKey, ItemValue>> {

companion object {
fun <ItemId : Any, PageRequestKey : Any, ItemValue : Any> empty(): MutableOperationPipeline<ItemId, PageRequestKey, ItemValue> {
return RealMutableOperationPipeline(mutableListOf())
}
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal interface OperationApplier<ItemId: Any, PageRequestKey: Any, ItemValue:
suspend fun applyOperations(
snapshot: ItemSnapshotList<ItemId, ItemValue>,
key: PageRequestKey?,
pagingState: PagingState<ItemId>,
pagingState: PagingState<ItemId, PageRequestKey, ItemValue>,
fetchingState: FetchingState<ItemId, PageRequestKey>
): ItemSnapshotList<ItemId, ItemValue>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import org.mobilenativefoundation.storex.paging.runtime.PagingState
/**
* Manages the paging state and provides methods to update it.
*/
internal interface PagingStateManager<ItemId: Any> {
val pagingState: StateFlow<PagingState<ItemId>>
internal interface PagingStateManager<ItemId : Any, PageRequestKey : Any, ItemValue : Any> {
val pagingState: StateFlow<PagingState<ItemId, PageRequestKey, ItemValue>>
fun updateWithAppendData(ids: List<ItemId?>, endOfPaginationReached: Boolean)
fun updateWithPrependData(ids: List<ItemId?>, endOfPaginationReached: Boolean)
fun updateWithAppendError(error: Throwable)
Expand Down
Loading

0 comments on commit b83ca75

Please sign in to comment.