Skip to content

Commit

Permalink
add ApolloClient.failFastIfOffline
Browse files Browse the repository at this point in the history
  • Loading branch information
martinbonnin committed Mar 13, 2024
1 parent 7b81b19 commit 8d9344c
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 11 deletions.
2 changes: 1 addition & 1 deletion libraries/apollo-api/api/apollo-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class com/apollographql/apollo3/api/ApolloOptionalAdapter : com/apo
}

public final class com/apollographql/apollo3/api/ApolloRequest : com/apollographql/apollo3/api/ExecutionOptions {
public synthetic fun <init> (Lcom/apollographql/apollo3/api/Operation;Ljava/util/UUID;Lcom/apollographql/apollo3/api/ExecutionContext;Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Lcom/apollographql/apollo3/api/Operation;Ljava/util/UUID;Lcom/apollographql/apollo3/api/ExecutionContext;Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getCanBeBatched ()Ljava/lang/Boolean;
public fun getEnableAutoPersistedQueries ()Ljava/lang/Boolean;
public fun getExecutionContext ()Lcom/apollographql/apollo3/api/ExecutionContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ private constructor(
override val canBeBatched: Boolean?,
@ApolloExperimental
val retryOnError: Boolean?,
@ApolloExperimental
val failFastIfOffline: Boolean?,
) : ExecutionOptions {

fun newBuilder(): Builder<D> = newBuilder(operation)
Expand All @@ -38,6 +40,7 @@ private constructor(
.enableAutoPersistedQueries(enableAutoPersistedQueries)
.canBeBatched(canBeBatched)
.retryOnError(retryOnError)
.failFastIfOffline(failFastIfOffline)
}

class Builder<D : Operation.Data>(
Expand All @@ -62,6 +65,14 @@ private constructor(
@ApolloExperimental
var retryOnError: Boolean? = null
private set
@ApolloExperimental
var failFastIfOffline: Boolean? = null
private set

@ApolloExperimental
fun failFastIfOffline(failFastIfOffline: Boolean?): Builder<D> = apply {
this.failFastIfOffline = failFastIfOffline
}

override fun httpMethod(httpMethod: HttpMethod?): Builder<D> = apply {
this.httpMethod = httpMethod
Expand Down Expand Up @@ -119,7 +130,8 @@ private constructor(
sendDocument = sendDocument,
enableAutoPersistedQueries = enableAutoPersistedQueries,
canBeBatched = canBeBatched,
retryOnError = retryOnError
retryOnError = retryOnError,
failFastIfOffline = failFastIfOffline,
)
}
}
Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-runtime/api/android/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public final class com/apollographql/apollo3/ApolloCall : com/apollographql/apol
public synthetic fun enableAutoPersistedQueries (Ljava/lang/Boolean;)Ljava/lang/Object;
public final fun execute (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun executeV3 (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun failFastIfOffline (Ljava/lang/Boolean;)Lcom/apollographql/apollo3/ApolloCall;
public fun getCanBeBatched ()Ljava/lang/Boolean;
public fun getEnableAutoPersistedQueries ()Ljava/lang/Boolean;
public fun getExecutionContext ()Lcom/apollographql/apollo3/api/ExecutionContext;
Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-runtime/api/jvm/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public final class com/apollographql/apollo3/ApolloCall : com/apollographql/apol
public synthetic fun enableAutoPersistedQueries (Ljava/lang/Boolean;)Ljava/lang/Object;
public final fun execute (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun executeV3 (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun failFastIfOffline (Ljava/lang/Boolean;)Lcom/apollographql/apollo3/ApolloCall;
public fun getCanBeBatched ()Ljava/lang/Boolean;
public fun getEnableAutoPersistedQueries ()Ljava/lang/Boolean;
public fun getExecutionContext ()Lcom/apollographql/apollo3/api/ExecutionContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class ApolloCall<D : Operation.Data> internal constructor(
@ApolloExperimental
var retryOnError: Boolean? = null
private set
@ApolloExperimental
var failFastIfOffline: Boolean? = null
private set

/**
* The HTTP headers to be sent with the request.
Expand All @@ -49,6 +52,10 @@ class ApolloCall<D : Operation.Data> internal constructor(
var ignoreApolloClientHttpHeaders: Boolean? = null
private set

fun failFastIfOffline(failFastIfOffline: Boolean?) = apply {
this.failFastIfOffline = failFastIfOffline
}

override fun addExecutionContext(executionContext: ExecutionContext) = apply {
this.executionContext = this.executionContext + executionContext
}
Expand Down Expand Up @@ -116,6 +123,7 @@ class ApolloCall<D : Operation.Data> internal constructor(
.enableAutoPersistedQueries(enableAutoPersistedQueries)
.canBeBatched(canBeBatched)
.retryOnError(retryOnError)
.failFastIfOffline(failFastIfOffline)
}

/**
Expand Down Expand Up @@ -163,6 +171,7 @@ class ApolloCall<D : Operation.Data> internal constructor(
.enableAutoPersistedQueries(enableAutoPersistedQueries)
.canBeBatched(canBeBatched)
.retryOnError(retryOnError)
.failFastIfOffline(failFastIfOffline)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ import com.apollographql.apollo3.interceptor.DefaultInterceptorChain
import com.apollographql.apollo3.interceptor.NetworkInterceptor
import com.apollographql.apollo3.interceptor.RetryOnErrorInterceptor
import com.apollographql.apollo3.internal.defaultDispatcher
import com.apollographql.apollo3.network.DefaultNetworkMonitor
import com.apollographql.apollo3.network.NetworkMonitor
import com.apollographql.apollo3.network.NetworkTransport
import com.apollographql.apollo3.network.http.BatchingHttpInterceptor
import com.apollographql.apollo3.network.http.HttpEngine
import com.apollographql.apollo3.network.http.HttpInterceptor
import com.apollographql.apollo3.network.http.HttpNetworkTransport
import com.apollographql.apollo3.network.platformConnectivityManager
import com.apollographql.apollo3.network.ws.WebSocketEngine
import com.apollographql.apollo3.network.ws.WebSocketNetworkTransport
import com.apollographql.apollo3.network.ws.WsProtocol
Expand Down Expand Up @@ -58,6 +56,7 @@ private constructor(
private val networkMonitor: NetworkMonitor?
private val retryOnError: ((ApolloRequest<*>) -> Boolean)? = builder.retryOnError
private val retryOnErrorInterceptor: ApolloInterceptor
private val failFastIfOffline = builder.failFastIfOffline

override val executionContext: ExecutionContext = builder.executionContext
override val httpMethod: HttpMethod? = builder.httpMethod
Expand Down Expand Up @@ -263,6 +262,12 @@ private constructor(
retryOnError = this@ApolloClient.retryOnError?.invoke(apolloRequest) ?: false
}
retryOnError(retryOnError)

var failFastIfOffline = apolloRequest.failFastIfOffline
if (failFastIfOffline == null) {
failFastIfOffline = this@ApolloClient.failFastIfOffline ?: false
}
failFastIfOffline(failFastIfOffline)
}
.build()

Expand Down Expand Up @@ -350,6 +355,21 @@ private constructor(
@ApolloExperimental
var retryOnError: ((ApolloRequest<*>) -> Boolean)? = null
private set
@ApolloExperimental
var failFastIfOffline: Boolean? = null
private set

/**
* Whether to fail fast if the device is offline.
*
* In that case, the returned [ApolloResponse.exception] is an instance of [com.apollographql.apollo3.exception.ApolloNetworkException]
*
* @see NetworkMonitor
*/
@ApolloExperimental
fun failFastIfOffline(failFastIfOffline: Boolean?): Builder = apply {
this.failFastIfOffline = failFastIfOffline
}

@ApolloExperimental
fun networkMonitor(networkMonitor: NetworkMonitor?): Builder = apply {
Expand Down Expand Up @@ -667,6 +687,7 @@ private constructor(
.retryOnError(retryOnError)
.retryOnErrorInterceptor(retryOnErrorInterceptor)
.networkMonitor(networkMonitor)
.failFastIfOffline(failFastIfOffline)
return builder
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import com.apollographql.apollo3.api.ApolloRequest
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.exception.ApolloException
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo3.exception.SubscriptionOperationException
import com.apollographql.apollo3.network.NetworkMonitor
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
Expand All @@ -18,10 +20,6 @@ import kotlin.time.Duration.Companion.seconds

internal class RetryOnErrorInterceptor(private val networkMonitor: NetworkMonitor?): ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
if (request.retryOnError != true) {
return chain.proceed(request)
}

var first = true
var attempt = 0
return flow {
Expand All @@ -31,9 +29,20 @@ internal class RetryOnErrorInterceptor(private val networkMonitor: NetworkMonito
} else {
request.newBuilder().requestUuid(uuid4()).build()
}

if (request.failFastIfOffline == true && networkMonitor?.isOnline == false) {
throw OfflineException
}

emitAll(chain.proceed(actualRequest))
}.catch {
if (it == OfflineException) {
emit(ApolloResponse.Builder(request.operation, request.requestUuid).exception(OfflineException).build())
} else {
throw it
}
}.onEach {
if (it.exception != null && it.exception!!.isTerminalAndRecoverable()) {
if (request.retryOnError == true && it.exception != null && it.exception!!.isTerminalAndRecoverable()) {
throw RetryException
} else {
attempt = 0
Expand Down Expand Up @@ -65,4 +74,5 @@ private fun ApolloException.isTerminalAndRecoverable(): Boolean {
}
}

private object RetryException: Exception()
private object RetryException: Exception()
private val OfflineException = ApolloNetworkException("The device is offline")
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.cash.turbine.test
import com.apollographql.apollo3.api.ApolloRequest
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo3.interceptor.ApolloInterceptor
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
import com.apollographql.apollo3.mockserver.assertNoRequest
Expand All @@ -21,8 +22,34 @@ import kotlinx.coroutines.flow.takeWhile
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertIs
import kotlin.test.assertNull

class NetworkMonitorTest {
@Test
fun failFastIfOfflineTest(): ApolloTestResult {
val fakeNetworkMonitor = FakeNetworkMonitor()

return mockServerTest(clientBuilder = {
networkMonitor(fakeNetworkMonitor)
failFastIfOffline(true)
}) {

fakeNetworkMonitor._isOnline.value = false

apolloClient.query(FooQuery()).toFlow()
.test {
awaitItem().apply {
assertNull(data)
assertIs<ApolloNetworkException>(exception)
assertEquals("The device is offline", exception?.message)
}
mockServer.assertNoRequest()
awaitComplete()
}
}
}

class NetworkMonitorInterceptorTest {
@Test
fun networkMonitorInterceptorTest(): ApolloTestResult {
val fakeNetworkMonitor = FakeNetworkMonitor()
Expand Down

0 comments on commit 8d9344c

Please sign in to comment.