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

Introduce Comparison Functions #99

Merged
merged 1 commit into from
Sep 22, 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
Expand Up @@ -97,7 +97,7 @@ suspend inline fun <T, S> QueryCommand.Context<QueryChunks<T, S>>.dispatchFetchC
.map { QueryChunk(it, variable) }
.map { chunk -> state.reply.getOrElse { emptyList() } + chunk }
.run { key.onRecoverData()?.let(::recoverCatching) ?: this }
.onSuccess(::dispatchFetchSuccess)
.onSuccess { dispatchFetchSuccess(it, key.contentEquals) }
.onFailure(::dispatchFetchFailure)
.onFailure { reportQueryError(it, key.id, marker) }
.also { callback?.invoke(it) }
Expand All @@ -122,7 +122,7 @@ suspend inline fun <T, S> QueryCommand.Context<QueryChunks<T, S>>.dispatchRevali
) {
revalidate(key, chunks)
.run { key.onRecoverData()?.let(::recoverCatching) ?: this }
.onSuccess(::dispatchFetchSuccess)
.onSuccess { dispatchFetchSuccess(it, key.contentEquals) }
.onFailure(::dispatchFetchFailure)
.onFailure { reportQueryError(it, key.id, marker) }
.also { callback?.invoke(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,24 @@ interface InfiniteQueryKey<T, S> {
*/
val loadMoreParam: (chunks: QueryChunks<T, S>) -> S?

/**
* Function to compare the content of the data.
*
* This function is used to determine whether the data is identical to the previous data via [InfiniteQueryCommand].
* If the data is considered the same, only [QueryState.staleAt] is updated.
* This can be useful when strict update management is needed, such as when special comparison is necessary,
* although it is generally not that important.
*
* @see QueryKey.contentEquals
*/
val contentEquals: QueryContentEquals<QueryChunks<T, S>>? get() = null

/**
* Function to configure the [QueryOptions].
*
* If unspecified, the default value of [SwrCachePolicy] is used.
*
* @see QueryKey.onConfigureOptions
*/
fun onConfigureOptions(): QueryOptionsOverride? = null

Expand All @@ -52,7 +66,7 @@ interface InfiniteQueryKey<T, S> {
*
* Depending on the type of exception that occurred during data retrieval, it is possible to recover it as normal data.
*
* @see QueryRecoverData
* @see QueryKey.onRecoverData
*/
fun onRecoverData(): QueryRecoverData<QueryChunks<T, S>>? = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fun <T> createMutationReducer(): MutationReducer<T> = { state, action ->
reply = Reply(action.data),
replyUpdatedAt = action.dataUpdatedAt,
error = null,
errorUpdatedAt = action.dataUpdatedAt,
errorUpdatedAt = if (state.error != null) action.dataUpdatedAt else state.errorUpdatedAt,
mutatedCount = state.mutatedCount + 1
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ interface MutationClient {
): MutationRef<T, S>
}

typealias MutationContentEquals<T> = (oldData: T, newData: T) -> Boolean
typealias MutationOptionsOverride = (MutationOptions) -> MutationOptions
typealias MutationCallback<T> = (Result<T>) -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import soil.query.core.ErrorRecord
import soil.query.core.Marker
import soil.query.core.Reply
import soil.query.core.RetryCallback
import soil.query.core.RetryFn
import soil.query.core.UniqueId
Expand Down Expand Up @@ -103,7 +104,7 @@ suspend inline fun <T, S> MutationCommand.Context<T>.dispatchMutateResult(
if (job != null && options.shouldExecuteEffectSynchronously) {
job.join()
}
dispatchMutateSuccess(data)
dispatchMutateSuccess(data, key.contentEquals)
}
}
.onFailure(::dispatchMutateFailure)
Expand All @@ -116,12 +117,23 @@ suspend inline fun <T, S> MutationCommand.Context<T>.dispatchMutateResult(
*
* @param data The mutation returned data.
*/
fun <T> MutationCommand.Context<T>.dispatchMutateSuccess(data: T) {
fun <T> MutationCommand.Context<T>.dispatchMutateSuccess(
data: T,
contentEquals: MutationContentEquals<T>? = null
) {
val currentAt = epoch()
val action = MutationAction.MutateSuccess(
data = data,
dataUpdatedAt = currentAt
)
val currentReply = state.reply
val action = if (currentReply is Reply.Some && contentEquals?.invoke(currentReply.value, data) == true) {
MutationAction.MutateSuccess(
data = currentReply.value,
dataUpdatedAt = state.replyUpdatedAt
)
} else {
MutationAction.MutateSuccess(
data = data,
dataUpdatedAt = currentAt
)
}
dispatch(action)
}

Expand All @@ -132,10 +144,18 @@ fun <T> MutationCommand.Context<T>.dispatchMutateSuccess(data: T) {
*/
fun <T> MutationCommand.Context<T>.dispatchMutateFailure(error: Throwable) {
val currentAt = epoch()
val action = MutationAction.MutateFailure(
error = error,
errorUpdatedAt = currentAt
)
val currentError = state.error
val action = if (currentError != null && options.errorEquals?.invoke(currentError, error) == true) {
MutationAction.MutateFailure(
error = currentError,
errorUpdatedAt = state.errorUpdatedAt
)
} else {
MutationAction.MutateFailure(
error = error,
errorUpdatedAt = currentAt
)
}
dispatch(action)
}

Expand Down
14 changes: 14 additions & 0 deletions soil-query-core/src/commonMain/kotlin/soil/query/MutationKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ interface MutationKey<T, S> {
*/
val mutate: suspend MutationReceiver.(variable: S) -> T

/**
* Function to compare the content of the data.
*
* This function is used to determine whether the data is identical to the previous data via [MutationCommand].
* If the data is considered the same, [MutationState.replyUpdatedAt] is not updated, and the existing reply state is maintained.
* This can be useful when strict update management is needed, such as when special comparison is necessary,
* although it is generally not that important.
*
* ```kotlin
* override val contentEquals: MutationContentEquals<SomeType> = { a, b -> a.xx == b.xx }
* ```
*/
val contentEquals: MutationContentEquals<T>? get() = null

/**
* Function to configure the [MutationOptions].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ interface MutationOptions : ActorOptions, LoggingOptions, RetryOptions {
*/
val isStrictMode: Boolean

/**
* Determines whether two errors are equal.
*
* This function is used to determine whether a new error is identical to an existing error via [MutationCommand].
* If the errors are considered identical, [MutationState.errorUpdatedAt] is not updated, and the existing error state is maintained.
*/
val errorEquals: ((Throwable, Throwable) -> Boolean)?

/**
* This callback function will be called if some mutation encounters an error.
*/
Expand All @@ -46,6 +54,7 @@ interface MutationOptions : ActorOptions, LoggingOptions, RetryOptions {
companion object Default : MutationOptions {
override val isOneShot: Boolean = false
override val isStrictMode: Boolean = false
override val errorEquals: ((Throwable, Throwable) -> Boolean)? = null
override val onError: ((ErrorRecord, MutationModel<*>) -> Unit)? = null
override val shouldSuppressErrorRelay: ((ErrorRecord, MutationModel<*>) -> Boolean)? = null
override val shouldExecuteEffectSynchronously: Boolean = false
Expand All @@ -72,6 +81,7 @@ interface MutationOptions : ActorOptions, LoggingOptions, RetryOptions {
*
* @param isOneShot Only allows mutate to execute once while active (until reset).
* @param isStrictMode Requires revision match as a precondition for executing mutate.
* @param errorEquals Determines whether two errors are equal.
* @param onError This callback function will be called if some mutation encounters an error.
* @param shouldSuppressErrorRelay Determines whether to suppress error information when relaying it using [soil.query.core.ErrorRelay].
* @param shouldExecuteEffectSynchronously Whether the query side effect should be synchronous.
Expand All @@ -88,6 +98,7 @@ interface MutationOptions : ActorOptions, LoggingOptions, RetryOptions {
fun MutationOptions(
isOneShot: Boolean = MutationOptions.isOneShot,
isStrictMode: Boolean = MutationOptions.isStrictMode,
errorEquals: ((Throwable, Throwable) -> Boolean)? = MutationOptions.errorEquals,
onError: ((ErrorRecord, MutationModel<*>) -> Unit)? = MutationOptions.onError,
shouldSuppressErrorRelay: ((ErrorRecord, MutationModel<*>) -> Boolean)? = MutationOptions.shouldSuppressErrorRelay,
shouldExecuteEffectSynchronously: Boolean = MutationOptions.shouldExecuteEffectSynchronously,
Expand All @@ -104,6 +115,7 @@ fun MutationOptions(
return object : MutationOptions {
override val isOneShot: Boolean = isOneShot
override val isStrictMode: Boolean = isStrictMode
override val errorEquals: ((Throwable, Throwable) -> Boolean)? = errorEquals
override val onError: ((ErrorRecord, MutationModel<*>) -> Unit)? = onError
override val shouldSuppressErrorRelay: ((ErrorRecord, MutationModel<*>) -> Boolean)? = shouldSuppressErrorRelay
override val shouldExecuteEffectSynchronously: Boolean = shouldExecuteEffectSynchronously
Expand All @@ -125,6 +137,7 @@ fun MutationOptions(
fun MutationOptions.copy(
isOneShot: Boolean = this.isOneShot,
isStrictMode: Boolean = this.isStrictMode,
errorEquals: ((Throwable, Throwable) -> Boolean)? = this.errorEquals,
onError: ((ErrorRecord, MutationModel<*>) -> Unit)? = this.onError,
shouldSuppressErrorRelay: ((ErrorRecord, MutationModel<*>) -> Boolean)? = this.shouldSuppressErrorRelay,
shouldExecuteEffectSynchronously: Boolean = this.shouldExecuteEffectSynchronously,
Expand All @@ -141,6 +154,7 @@ fun MutationOptions.copy(
return MutationOptions(
isOneShot = isOneShot,
isStrictMode = isStrictMode,
errorEquals = errorEquals,
onError = onError,
shouldSuppressErrorRelay = shouldSuppressErrorRelay,
shouldExecuteEffectSynchronously = shouldExecuteEffectSynchronously,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ fun <T> createQueryReducer(): QueryReducer<T> = { state, action ->
reply = Reply(action.data),
replyUpdatedAt = action.dataUpdatedAt,
error = null,
errorUpdatedAt = action.dataUpdatedAt,
errorUpdatedAt = if (state.error != null) action.dataUpdatedAt else state.errorUpdatedAt,
staleAt = action.dataStaleAt,
status = QueryStatus.Success,
fetchStatus = QueryFetchStatus.Idle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ interface QueryMutableClient : QueryReadonlyClient {
typealias QueryInitialData<T> = QueryReadonlyClient.() -> T?
typealias QueryEffect = QueryMutableClient.() -> Unit

typealias QueryContentEquals<T> = (oldData: T, newData: T) -> Boolean
typealias QueryRecoverData<T> = (error: Throwable) -> T
typealias QueryOptionsOverride = (QueryOptions) -> QueryOptions
typealias QueryCallback<T> = (Result<T>) -> Unit
46 changes: 34 additions & 12 deletions soil-query-core/src/commonMain/kotlin/soil/query/QueryCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package soil.query

import soil.query.core.ErrorRecord
import soil.query.core.Marker
import soil.query.core.Reply
import soil.query.core.RetryCallback
import soil.query.core.RetryFn
import soil.query.core.UniqueId
Expand Down Expand Up @@ -111,7 +112,7 @@ suspend inline fun <T> QueryCommand.Context<T>.dispatchFetchResult(
) {
fetch(key)
.run { key.onRecoverData()?.let(::recoverCatching) ?: this }
.onSuccess(::dispatchFetchSuccess)
.onSuccess { dispatchFetchSuccess(it, key.contentEquals) }
.onFailure(::dispatchFetchFailure)
.onFailure { reportQueryError(it, key.id, marker) }
.also { callback?.invoke(it) }
Expand All @@ -122,13 +123,25 @@ suspend inline fun <T> QueryCommand.Context<T>.dispatchFetchResult(
*
* @param data The fetched data.
*/
fun <T> QueryCommand.Context<T>.dispatchFetchSuccess(data: T) {
fun <T> QueryCommand.Context<T>.dispatchFetchSuccess(
data: T,
contentEquals: QueryContentEquals<T>? = null
) {
val currentAt = epoch()
val action = QueryAction.FetchSuccess(
data = data,
dataUpdatedAt = currentAt,
dataStaleAt = options.staleTime.toEpoch(currentAt)
)
val currentReply = state.reply
val action = if (currentReply is Reply.Some && contentEquals?.invoke(currentReply.value, data) == true) {
QueryAction.FetchSuccess(
data = currentReply.value,
dataUpdatedAt = state.replyUpdatedAt,
dataStaleAt = options.staleTime.toEpoch(currentAt)
)
} else {
QueryAction.FetchSuccess(
data = data,
dataUpdatedAt = currentAt,
dataStaleAt = options.staleTime.toEpoch(currentAt)
)
}
dispatch(action)
}

Expand All @@ -139,11 +152,20 @@ fun <T> QueryCommand.Context<T>.dispatchFetchSuccess(data: T) {
*/
fun <T> QueryCommand.Context<T>.dispatchFetchFailure(error: Throwable) {
val currentAt = epoch()
val action = QueryAction.FetchFailure(
error = error,
errorUpdatedAt = currentAt,
paused = shouldPause(error)
)
val currentError = state.error
val action = if (currentError != null && options.errorEquals?.invoke(currentError, error) == true) {
QueryAction.FetchFailure(
error = currentError,
errorUpdatedAt = state.errorUpdatedAt,
paused = shouldPause(currentError)
)
} else {
QueryAction.FetchFailure(
error = error,
errorUpdatedAt = currentAt,
paused = shouldPause(error)
)
}
dispatch(action)
}

Expand Down
14 changes: 14 additions & 0 deletions soil-query-core/src/commonMain/kotlin/soil/query/QueryKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ interface QueryKey<T> {
*/
val fetch: suspend QueryReceiver.() -> T

/**
* Function to compare the content of the data.
*
* This function is used to determine whether the data is identical to the previous data via [QueryCommand].
* If the data is considered the same, only [QueryState.staleAt] is updated.
* This can be useful when strict update management is needed, such as when special comparison is necessary,
* although it is generally not that important.
*
* ```kotlin
* override val contentEquals: QueryContentEquals<SomeType> = { a, b -> a.xx == b.xx }
* ```
*/
val contentEquals: QueryContentEquals<T>? get() = null

/**
* Function to configure the [QueryOptions].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ interface QueryOptions : ActorOptions, LoggingOptions, RetryOptions {
*/
val prefetchWindowTime: Duration

/**
* Determines whether two errors are equal.
*
* This function is used to determine whether a new error is identical to an existing error via [QueryCommand].
* If the errors are considered identical, [QueryState.errorUpdatedAt] is not updated, and the existing error state is maintained.
*/
val errorEquals: ((Throwable, Throwable) -> Boolean)?

/**
* Determines whether query processing needs to be paused based on error.
*
Expand Down Expand Up @@ -75,6 +83,7 @@ interface QueryOptions : ActorOptions, LoggingOptions, RetryOptions {
override val staleTime: Duration = Duration.ZERO
override val gcTime: Duration = 5.minutes
override val prefetchWindowTime: Duration = 1.seconds
override val errorEquals: ((Throwable, Throwable) -> Boolean)? = null
override val pauseDurationAfter: ((Throwable) -> Duration?)? = null
override val revalidateOnReconnect: Boolean = true
override val revalidateOnFocus: Boolean = true
Expand Down Expand Up @@ -106,6 +115,7 @@ interface QueryOptions : ActorOptions, LoggingOptions, RetryOptions {
* @param staleTime The duration after which the returned value of the fetch function block is considered stale.
* @param gcTime The period during which the Key's return value, if not referenced anywhere, is temporarily cached in memory.
* @param prefetchWindowTime Maximum window time on prefetch processing.
* @param errorEquals Determines whether two errors are equal.
* @param pauseDurationAfter Determines whether query processing needs to be paused based on error.
* @param revalidateOnReconnect Automatically revalidate active [Query] when the network reconnects.
* @param revalidateOnFocus Automatically revalidate active [Query] when the window is refocused.
Expand All @@ -125,6 +135,7 @@ fun QueryOptions(
staleTime: Duration = QueryOptions.staleTime,
gcTime: Duration = QueryOptions.gcTime,
prefetchWindowTime: Duration = QueryOptions.prefetchWindowTime,
errorEquals: ((Throwable, Throwable) -> Boolean)? = QueryOptions.errorEquals,
pauseDurationAfter: ((Throwable) -> Duration?)? = QueryOptions.pauseDurationAfter,
revalidateOnReconnect: Boolean = QueryOptions.revalidateOnReconnect,
revalidateOnFocus: Boolean = QueryOptions.revalidateOnFocus,
Expand All @@ -144,6 +155,7 @@ fun QueryOptions(
override val staleTime: Duration = staleTime
override val gcTime: Duration = gcTime
override val prefetchWindowTime: Duration = prefetchWindowTime
override val errorEquals: ((Throwable, Throwable) -> Boolean)? = errorEquals
override val pauseDurationAfter: ((Throwable) -> Duration?)? = pauseDurationAfter
override val revalidateOnReconnect: Boolean = revalidateOnReconnect
override val revalidateOnFocus: Boolean = revalidateOnFocus
Expand All @@ -168,6 +180,7 @@ fun QueryOptions.copy(
staleTime: Duration = this.staleTime,
gcTime: Duration = this.gcTime,
prefetchWindowTime: Duration = this.prefetchWindowTime,
errorEquals: ((Throwable, Throwable) -> Boolean)? = this.errorEquals,
pauseDurationAfter: ((Throwable) -> Duration?)? = this.pauseDurationAfter,
revalidateOnReconnect: Boolean = this.revalidateOnReconnect,
revalidateOnFocus: Boolean = this.revalidateOnFocus,
Expand All @@ -187,6 +200,7 @@ fun QueryOptions.copy(
staleTime = staleTime,
gcTime = gcTime,
prefetchWindowTime = prefetchWindowTime,
errorEquals = errorEquals,
pauseDurationAfter = pauseDurationAfter,
revalidateOnReconnect = revalidateOnReconnect,
revalidateOnFocus = revalidateOnFocus,
Expand Down
Loading