Skip to content

Commit

Permalink
Add back (legacy) throwing FetchPolicy interceptors, and throwApolloE…
Browse files Browse the repository at this point in the history
…xceptions parameter, for migration convenience
  • Loading branch information
BoD committed Feb 14, 2023
1 parent 3d3ece9 commit 1cedd6c
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@ public final class com/apollographql/apollo3/cache/normalized/NormalizedCache {
public static final fun writeToCacheAsynchronously (Lcom/apollographql/apollo3/api/MutableExecutionOptions;Z)Ljava/lang/Object;
}

public final class com/apollographql/apollo3/cache/normalized/ThrowingFetchPolicyInterceptorsKt {
public static final fun getThrowingCacheAndNetworkInterceptor ()Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;
public static final fun getThrowingCacheFirstInterceptor ()Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;
public static final fun getThrowingNetworkFirstInterceptor ()Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.apollographql.apollo3.cache.normalized

import com.apollographql.apollo3.api.ApolloRequest
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.exception.ApolloCompositeException
import com.apollographql.apollo3.exception.ApolloException
import com.apollographql.apollo3.exception.CacheMissException
import com.apollographql.apollo3.interceptor.ApolloInterceptor
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single

/**
* An interceptor that goes to cache first and then to the network if it fails.
* Network errors are thrown as an [ApolloCompositeException] with the cache error as the first cause.
*
* This is the same behavior as CacheFirstInterceptor in 3.x and is kept here as a convenience for migration.
*/
val ThrowingCacheFirstInterceptor = object : ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
return flow {
val cacheException: ApolloException?
var networkException: ApolloException? = null

val cacheResponse = chain.proceed(
request = request
.newBuilder()
.fetchFromCache(true)
.build()
).single()
cacheException = cacheResponse.exception

if (cacheException == null) {
emit(cacheResponse)
return@flow
}

val networkResponses = chain.proceed(
request = request
).onEach { response ->
if (networkException == null) networkException = response.exception
}.filter { response ->
response.exception == null
}.map { response ->
response.newBuilder()
.cacheInfo(
response.cacheInfo!!
.newBuilder()
.cacheMissException(cacheException as? CacheMissException)
.build()
)
.build()
}

emitAll(networkResponses)

if (networkException != null) {
throw ApolloCompositeException(
first = cacheException,
second = networkException
)
}
}
}
}

/**
* An interceptor that goes to the network first and then to cache if it fails.
* Cache errors are thrown as an [ApolloCompositeException] with the network error as the first cause.
*
* This is the same behavior as NetworkFirstInterceptor in 3.x and is kept here as a convenience for migration.
*/
val ThrowingNetworkFirstInterceptor = object : ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
return flow {
val cacheException: ApolloException?
var networkException: ApolloException? = null

val networkResponses = chain.proceed(
request = request
).onEach { response ->
if (networkException == null) networkException = response.exception
}.filter { response ->
response.exception == null
}

emitAll(networkResponses)
if (networkException == null) {
return@flow
}

val cacheResponse = chain.proceed(
request = request
.newBuilder()
.fetchFromCache(true)
.build()
).single()
cacheException = cacheResponse.exception

if (cacheException == null) {
emit(
cacheResponse.newBuilder()
.cacheInfo(
cacheResponse.cacheInfo!!
.newBuilder()
.networkException(networkException)
.build()
)
.build()
)
return@flow
}

throw ApolloCompositeException(
first = networkException,
second = cacheException,
)
}
}
}

/**
* An interceptor that goes to cache first and then to the network.
* An exception is not thrown if the cache fails, whereas an exception will be thrown upon network failure.
*
* This is the same behavior as CacheAndNetworkInterceptor in 3.x and is kept here as a convenience for migration.
*/
val ThrowingCacheAndNetworkInterceptor = object : ApolloInterceptor {
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
return flow {
val cacheException: ApolloException?
var networkException: ApolloException? = null

val cacheResponse = chain.proceed(
request = request
.newBuilder()
.fetchFromCache(true)
.build()
).single()
cacheException = cacheResponse.exception

if (cacheException == null) {
emit(cacheResponse.newBuilder().isLast(false).build())
}

val networkResponses = chain.proceed(request)
.onEach { response ->
if (networkException == null) networkException = response.exception
}
.map { response ->
response.newBuilder()
.cacheInfo(
response.cacheInfo!!
.newBuilder()
.cacheMissException(cacheException as? CacheMissException)
.build()
)
.build()
}
.filter { response ->
response.exception == null
}

emitAll(networkResponses)

if (networkException != null) {
if (cacheException != null) {
throw ApolloCompositeException(
first = cacheException,
second = networkException
)
}
throw networkException!!
}
}
}
}
3 changes: 2 additions & 1 deletion libraries/apollo-runtime/api/apollo-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public final class com/apollographql/apollo3/ApolloCall : com/apollographql/apol
public fun setHttpMethod (Lcom/apollographql/apollo3/api/http/HttpMethod;)V
public fun setSendApqExtensions (Ljava/lang/Boolean;)V
public fun setSendDocument (Ljava/lang/Boolean;)V
public final fun toFlow ()Lkotlinx/coroutines/flow/Flow;
public final fun toFlow (Z)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun toFlow$default (Lcom/apollographql/apollo3/ApolloCall;ZILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
}

public final class com/apollographql/apollo3/ApolloClient : com/apollographql/apollo3/api/ExecutionOptions, java/io/Closeable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.apollographql.apollo3.api.http.HttpMethod
import com.apollographql.apollo3.exception.ApolloCompositeException
import com.apollographql.apollo3.exception.ApolloException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList

/**
Expand Down Expand Up @@ -84,8 +85,10 @@ class ApolloCall<D : Operation.Data> internal constructor(
* println("order received: ${it.data?.order?.id"})
* }
* ```
* @param throwApolloExceptions whether to throw [ApolloException]s (e.g. network errors, cache misses) encountered in responses,
* rather than emitting them in [ApolloResponse.exception]. This was the behavior in 3.x and is kept here as a convenience for migration.
*/
fun toFlow(): Flow<ApolloResponse<D>> {
fun toFlow(throwApolloExceptions: Boolean = false): Flow<ApolloResponse<D>> {
val request = ApolloRequest.Builder(operation)
.executionContext(executionContext)
.httpMethod(httpMethod)
Expand All @@ -95,13 +98,25 @@ class ApolloCall<D : Operation.Data> internal constructor(
.enableAutoPersistedQueries(enableAutoPersistedQueries)
.canBeBatched(canBeBatched)
.build()
return apolloClient.executeAsFlow(request)
return apolloClient.executeAsFlow(request).let {
if (throwApolloExceptions) {
it.onEach { response ->
if (response.exception != null) {
throw response.exception!!
}
}
} else {
it
}
}
}

/**
* A shorthand for `toFlow().single()`.
* Use this for queries and mutation to get a single [ApolloResponse] from the network or the cache.
* For subscriptions or operations with `@defer`, you usually want to use [toFlow] instead to listen to all values.
* For subscriptions or operations using `@defer`, you usually want to use [toFlow] instead to listen to all values.
*
* Exceptions (e.g. network errors, cache misses) are rethrown by this method.
*/
suspend fun execute(): ApolloResponse<D> {
val responses = toFlow().toList()
Expand Down
1 change: 1 addition & 0 deletions tests/integration-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ kotlin {
because("OperationOutputTest uses it to check the json and we can't use moshi since it's mpp code")
}
implementation(golatac.lib("kotlinx.coroutines.test"))
implementation(golatac.lib("turbine"))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import com.apollographql.apollo3.integration.normalizer.HeroNameQuery
import com.apollographql.apollo3.mockserver.MockServer
import com.apollographql.apollo3.mockserver.enqueue
import com.apollographql.apollo3.testing.internal.runTest
import kotlinx.coroutines.flow.toList
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class ExceptionsTest {
Expand Down Expand Up @@ -59,4 +62,26 @@ class ExceptionsTest {
val exception = result.exceptionOrNull()
assertTrue(exception is ApolloNetworkException)
}

@Test
fun toFlowThrows() = runTest(before = { setUp() }, after = { tearDown() }) {
mockServer.enqueue("malformed")

val result = kotlin.runCatching {
apolloClient.query(HeroNameQuery()).toFlow(throwApolloExceptions = true).toList()
}

assertNotNull(result.exceptionOrNull())
}

@Test
fun toFlowDoesntThrow() = runTest(before = { setUp() }, after = { tearDown() }) {
mockServer.enqueue("malformed")

val result = kotlin.runCatching {
apolloClient.query(HeroNameQuery()).toFlow(throwApolloExceptions = false).toList()
}

assertNull(result.exceptionOrNull())
}
}
Loading

0 comments on commit 1cedd6c

Please sign in to comment.