diff --git a/apollo-normalized-cache/api/apollo-normalized-cache.api b/apollo-normalized-cache/api/apollo-normalized-cache.api index edfd72865c..11397758c5 100644 --- a/apollo-normalized-cache/api/apollo-normalized-cache.api +++ b/apollo-normalized-cache/api/apollo-normalized-cache.api @@ -106,6 +106,7 @@ public final class com/apollographql/apollo3/cache/normalized/NormalizedCache { public static final fun configureApolloClientBuilder (Lcom/apollographql/apollo3/ApolloClient$Builder;Lcom/apollographql/apollo3/cache/normalized/api/NormalizedCacheFactory;Lcom/apollographql/apollo3/cache/normalized/api/CacheKeyGenerator;Lcom/apollographql/apollo3/cache/normalized/api/CacheResolver;Z)Lcom/apollographql/apollo3/ApolloClient$Builder; public static synthetic fun configureApolloClientBuilder$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lcom/apollographql/apollo3/cache/normalized/api/NormalizedCacheFactory;Lcom/apollographql/apollo3/cache/normalized/api/CacheKeyGenerator;Lcom/apollographql/apollo3/cache/normalized/api/CacheResolver;ZILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; public static final fun doNotStore (Lcom/apollographql/apollo3/api/MutableExecutionOptions;Z)Ljava/lang/Object; + public static final fun emitCacheMisses (Lcom/apollographql/apollo3/api/MutableExecutionOptions;Z)Ljava/lang/Object; public static final fun executeCacheAndNetwork (Lcom/apollographql/apollo3/ApolloCall;)Lkotlinx/coroutines/flow/Flow; public static final fun fetchPolicy (Lcom/apollographql/apollo3/api/MutableExecutionOptions;Lcom/apollographql/apollo3/cache/normalized/FetchPolicy;)Ljava/lang/Object; public static final fun fetchPolicyInterceptor (Lcom/apollographql/apollo3/api/MutableExecutionOptions;Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;)Ljava/lang/Object; diff --git a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt index 2beae8b82b..c05c0ce234 100644 --- a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/ClientCacheExtensions.kt @@ -284,6 +284,17 @@ fun MutableExecutionOptions.doNotStore(doNotStore: Boolean) = addExecutio DoNotStoreContext(doNotStore) ) +/** + * @param emitCacheMisses Whether to emit cache misses instead of throwing. + * The returned response will have `response.data == null` + * You can read `response.cacheInfo` to get more information about the cache miss + * + * Default: false + */ +fun MutableExecutionOptions.emitCacheMisses(emitCacheMisses: Boolean) = addExecutionContext( + EmitCacheMissesContext(emitCacheMisses) +) + /** * @param storePartialResponses Whether to store partial responses. * @@ -341,6 +352,9 @@ internal val ApolloRequest.doNotStore internal val ApolloRequest.storePartialResponses get() = executionContext[StorePartialResponsesContext]?.value ?: false +internal val ApolloRequest.emitCacheMisses + get() = executionContext[EmitCacheMissesContext]?.value ?: false + internal val ApolloRequest.writeToCacheAsynchronously get() = executionContext[WriteToCacheAsynchronouslyContext]?.value ?: false @@ -523,6 +537,13 @@ internal class CacheHeadersContext(val value: CacheHeaders) : ExecutionContext.E companion object Key : ExecutionContext.Key } +internal class EmitCacheMissesContext(val value: Boolean) : ExecutionContext.Element { + override val key: ExecutionContext.Key<*> + get() = Key + + companion object Key : ExecutionContext.Key +} + internal class OptimisticUpdatesContext(val value: D) : ExecutionContext.Element { override val key: ExecutionContext.Key<*> get() = Key diff --git a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/internal/ApolloCacheInterceptor.kt b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/internal/ApolloCacheInterceptor.kt index 388933e34c..5bce466181 100644 --- a/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/internal/ApolloCacheInterceptor.kt +++ b/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/internal/ApolloCacheInterceptor.kt @@ -13,11 +13,13 @@ import com.apollographql.apollo3.cache.normalized.CacheInfo import com.apollographql.apollo3.cache.normalized.cacheHeaders import com.apollographql.apollo3.cache.normalized.cacheInfo import com.apollographql.apollo3.cache.normalized.doNotStore +import com.apollographql.apollo3.cache.normalized.emitCacheMisses import com.apollographql.apollo3.cache.normalized.fetchFromCache import com.apollographql.apollo3.cache.normalized.optimisticData import com.apollographql.apollo3.cache.normalized.storePartialResponses import com.apollographql.apollo3.cache.normalized.writeToCacheAsynchronously 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 com.apollographql.apollo3.mpp.currentTimeMillis @@ -178,11 +180,33 @@ internal class ApolloCacheInterceptor( val operation = request.operation val startMillis = currentTimeMillis() - val data = store.readOperation( - operation = operation, - customScalarAdapters = customScalarAdapters, - cacheHeaders = request.cacheHeaders - ) + val data = try { + store.readOperation( + operation = operation, + customScalarAdapters = customScalarAdapters, + cacheHeaders = request.cacheHeaders + ) + } catch (e: CacheMissException) { + if (request.emitCacheMisses) { + return ApolloResponse.Builder( + requestUuid = request.requestUuid, + operation = operation, + data = null, + ).addExecutionContext(request.executionContext) + .cacheInfo( + CacheInfo.Builder() + .cacheStartMillis(startMillis) + .cacheEndMillis(currentTimeMillis()) + .cacheHit(false) + .cacheMissException(e) + .build() + ) + .isLast(true) + .build() + } else { + throw e + } + } return ApolloResponse.Builder( requestUuid = request.requestUuid, diff --git a/tests/integration-tests/src/commonTest/kotlin/test/WatcherTest.kt b/tests/integration-tests/src/commonTest/kotlin/test/WatcherTest.kt index d2434df95f..5768abb7d6 100644 --- a/tests/integration-tests/src/commonTest/kotlin/test/WatcherTest.kt +++ b/tests/integration-tests/src/commonTest/kotlin/test/WatcherTest.kt @@ -8,6 +8,7 @@ import com.apollographql.apollo3.cache.normalized.ApolloStore import com.apollographql.apollo3.cache.normalized.FetchPolicy import com.apollographql.apollo3.cache.normalized.api.CacheHeaders import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory +import com.apollographql.apollo3.cache.normalized.emitCacheMisses import com.apollographql.apollo3.cache.normalized.fetchPolicy import com.apollographql.apollo3.cache.normalized.normalizedCache import com.apollographql.apollo3.cache.normalized.refetchPolicy @@ -41,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.fail @OptIn(ApolloExperimental::class) @@ -106,6 +108,34 @@ class WatcherTest { } } + @Test + fun emitCacheMissesIsWorking() = runTest(before = { setUp() }) { + val query = EpisodeHeroNameQuery(Episode.EMPIRE) + val channel = Channel() + + apolloClient.enqueueTestResponse(query, episodeHeroNameData) + + val job = launch { + apolloClient.query(query) + .fetchPolicy(FetchPolicy.CacheOnly) + .emitCacheMisses(true) + .watch() + .collect { + channel.send(it.data) + } + } + + val data = channel.receiveOrTimeout() + assertNull(data) + + // Update the cache + apolloClient.query(query).fetchPolicy(FetchPolicy.NetworkOnly).execute() + + assertEquals(channel.receiveOrTimeout()?.hero?.name, "R2-D2") + + job.cancel() + } + /** * Writing to the store out of band should update the watcher