diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.api b/normalized-cache-incubating/api/normalized-cache-incubating.api index b37ead63..dabf34e6 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.api @@ -120,6 +120,7 @@ public final class com/apollographql/cache/normalized/NormalizedCache { public static final fun getCacheHeaders (Lcom/apollographql/apollo/api/ApolloResponse;)Lcom/apollographql/cache/normalized/api/CacheHeaders; public static final fun getCacheInfo (Lcom/apollographql/apollo/api/ApolloResponse;)Lcom/apollographql/cache/normalized/CacheInfo; public static final fun isFromCache (Lcom/apollographql/apollo/api/ApolloResponse;)Z + public static final fun memoryCacheOnly (Lcom/apollographql/apollo/api/MutableExecutionOptions;Z)Ljava/lang/Object; public static final fun optimisticUpdates (Lcom/apollographql/apollo/ApolloCall;Lcom/apollographql/apollo/api/Mutation$Data;)Lcom/apollographql/apollo/ApolloCall; public static final fun optimisticUpdates (Lcom/apollographql/apollo/api/ApolloRequest$Builder;Lcom/apollographql/apollo/api/Mutation$Data;)Lcom/apollographql/apollo/api/ApolloRequest$Builder; public static final fun refetchPolicy (Lcom/apollographql/apollo/api/MutableExecutionOptions;Lcom/apollographql/cache/normalized/FetchPolicy;)Ljava/lang/Object; @@ -146,6 +147,7 @@ public final class com/apollographql/cache/normalized/api/ApolloCacheHeaders { public static final field EVICT_AFTER_READ Ljava/lang/String; public static final field INSTANCE Lcom/apollographql/cache/normalized/api/ApolloCacheHeaders; public static final field MAX_STALE Ljava/lang/String; + public static final field MEMORY_CACHE_ONLY Ljava/lang/String; } public abstract interface class com/apollographql/cache/normalized/api/ApolloResolver { diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api index 7c3d4306..93fcb0bd 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api @@ -383,6 +383,7 @@ final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOption final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/emitCacheMisses(kotlin/Boolean): com.apollographql.apollo.api/MutableExecutionOptions<#A> // com.apollographql.cache.normalized/emitCacheMisses|emitCacheMisses@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(kotlin.Boolean){0§}[0] final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/fetchPolicy(com.apollographql.cache.normalized/FetchPolicy): #A // com.apollographql.cache.normalized/fetchPolicy|fetchPolicy@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(com.apollographql.cache.normalized.FetchPolicy){0§}[0] final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/fetchPolicyInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor): #A // com.apollographql.cache.normalized/fetchPolicyInterceptor|fetchPolicyInterceptor@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(com.apollographql.apollo.interceptor.ApolloInterceptor){0§}[0] +final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/memoryCacheOnly(kotlin/Boolean): #A // com.apollographql.cache.normalized/memoryCacheOnly|memoryCacheOnly@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(kotlin.Boolean){0§}[0] final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/refetchPolicy(com.apollographql.cache.normalized/FetchPolicy): #A // com.apollographql.cache.normalized/refetchPolicy|refetchPolicy@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(com.apollographql.cache.normalized.FetchPolicy){0§}[0] final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/refetchPolicyInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor): #A // com.apollographql.cache.normalized/refetchPolicyInterceptor|refetchPolicyInterceptor@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(com.apollographql.apollo.interceptor.ApolloInterceptor){0§}[0] final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.cache.normalized/storeExpirationDate(kotlin/Boolean): #A // com.apollographql.cache.normalized/storeExpirationDate|storeExpirationDate@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(kotlin.Boolean){0§}[0] @@ -408,6 +409,8 @@ final object com.apollographql.cache.normalized.api/ApolloCacheHeaders { // com. final fun (): kotlin/String // com.apollographql.cache.normalized.api/ApolloCacheHeaders.EVICT_AFTER_READ.|(){}[0] final const val MAX_STALE // com.apollographql.cache.normalized.api/ApolloCacheHeaders.MAX_STALE|{}MAX_STALE[0] final fun (): kotlin/String // com.apollographql.cache.normalized.api/ApolloCacheHeaders.MAX_STALE.|(){}[0] + final const val MEMORY_CACHE_ONLY // com.apollographql.cache.normalized.api/ApolloCacheHeaders.MEMORY_CACHE_ONLY|{}MEMORY_CACHE_ONLY[0] + final fun (): kotlin/String // com.apollographql.cache.normalized.api/ApolloCacheHeaders.MEMORY_CACHE_ONLY.|(){}[0] } final object com.apollographql.cache.normalized.api/DefaultApolloResolver : com.apollographql.cache.normalized.api/ApolloResolver { // com.apollographql.cache.normalized.api/DefaultApolloResolver|null[0] final fun resolveField(com.apollographql.cache.normalized.api/ResolverContext): kotlin/Any? // com.apollographql.cache.normalized.api/DefaultApolloResolver.resolveField|resolveField(com.apollographql.cache.normalized.api.ResolverContext){}[0] diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/ClientCacheExtensions.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/ClientCacheExtensions.kt index 45909546..3416d8dd 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/ClientCacheExtensions.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/ClientCacheExtensions.kt @@ -316,6 +316,15 @@ fun MutableExecutionOptions.doNotStore(doNotStore: Boolean) = addExecutio DoNotStoreContext(doNotStore) ) +/** + * @param memoryCacheOnly Whether to store and read from a memory cache only. + * + * Default: false + */ +fun MutableExecutionOptions.memoryCacheOnly(memoryCacheOnly: Boolean) = addExecutionContext( + MemoryCacheOnlyContext(memoryCacheOnly) +) + @Deprecated("Emitting cache misses is now the default behavior, this method is a no-op", replaceWith = ReplaceWith("")) @ApolloDeprecatedSince(v4_0_0) @Suppress("UNUSED_PARAMETER") @@ -443,6 +452,9 @@ private val MutableExecutionOptions.refetchPolicyInterceptor internal val ApolloRequest.doNotStore get() = executionContext[DoNotStoreContext]?.value ?: false +internal val ApolloRequest.memoryCacheOnly + get() = executionContext[MemoryCacheOnlyContext]?.value ?: false + internal val ApolloRequest.storePartialResponses get() = executionContext[StorePartialResponsesContext]?.value ?: false @@ -618,6 +630,13 @@ internal class DoNotStoreContext(val value: Boolean) : ExecutionContext.Element companion object Key : ExecutionContext.Key } +internal class MemoryCacheOnlyContext(val value: Boolean) : ExecutionContext.Element { + override val key: ExecutionContext.Key<*> + get() = Key + + companion object Key : ExecutionContext.Key +} + internal class StorePartialResponsesContext(val value: Boolean) : ExecutionContext.Element { override val key: ExecutionContext.Key<*> get() = Key diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/ApolloCacheHeaders.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/ApolloCacheHeaders.kt index efc17b7e..a4fe3301 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/ApolloCacheHeaders.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/ApolloCacheHeaders.kt @@ -11,6 +11,11 @@ object ApolloCacheHeaders { */ const val DO_NOT_STORE = "do-not-store" + /** + * Records should be stored and read from the [MemoryCache] only. + */ + const val MEMORY_CACHE_ONLY = "memory-cache-only" + /** * Records from this request should be evicted after being read. */ diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/internal/ApolloCacheInterceptor.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/internal/ApolloCacheInterceptor.kt index e1c48783..a6716b92 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/internal/ApolloCacheInterceptor.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/internal/ApolloCacheInterceptor.kt @@ -24,6 +24,7 @@ import com.apollographql.cache.normalized.cacheHeaders import com.apollographql.cache.normalized.cacheInfo import com.apollographql.cache.normalized.doNotStore import com.apollographql.cache.normalized.fetchFromCache +import com.apollographql.cache.normalized.memoryCacheOnly import com.apollographql.cache.normalized.optimisticData import com.apollographql.cache.normalized.storePartialResponses import com.apollographql.cache.normalized.storeReceiveDate @@ -78,6 +79,9 @@ internal class ApolloCacheInterceptor( if (request.storeReceiveDate) { cacheHeaders += nowDateCacheHeaders() } + if (request.memoryCacheOnly) { + cacheHeaders += CacheHeaders.Builder().addHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY, "true").build() + } store.writeOperation(request.operation, response.data!!, customScalarAdapters, cacheHeaders) } else { emptySet() @@ -205,10 +209,14 @@ internal class ApolloCacheInterceptor( val startMillis = currentTimeMillis() val data = try { + var cacheHeaders = request.cacheHeaders + if (request.memoryCacheOnly) { + cacheHeaders += CacheHeaders.Builder().addHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY, "true").build() + } store.readOperation( operation = operation, customScalarAdapters = customScalarAdapters, - cacheHeaders = request.cacheHeaders + cacheHeaders = cacheHeaders, ) } catch (e: CacheMissException) { return ApolloResponse.Builder( diff --git a/normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt b/normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt index 0e0a7f2e..85466f22 100644 --- a/normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt +++ b/normalized-cache-sqlite-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/sql/SqlNormalizedCache.kt @@ -27,6 +27,9 @@ class SqlNormalizedCache internal constructor( } override fun loadRecord(key: String, cacheHeaders: CacheHeaders): Record? { + if (cacheHeaders.hasHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY)) { + return null + } val evictAfterRead = cacheHeaders.hasHeader(EVICT_AFTER_READ) return maybeTransaction(evictAfterRead) { try { @@ -44,6 +47,9 @@ class SqlNormalizedCache internal constructor( } override fun loadRecords(keys: Collection, cacheHeaders: CacheHeaders): Collection { + if (cacheHeaders.hasHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY)) { + return emptyList() + } val evictAfterRead = cacheHeaders.hasHeader(EVICT_AFTER_READ) return maybeTransaction(evictAfterRead) { try { @@ -88,7 +94,7 @@ class SqlNormalizedCache internal constructor( @ApolloExperimental override fun merge(record: Record, cacheHeaders: CacheHeaders, recordMerger: RecordMerger): Set { - if (cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE)) { + if (cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE) || cacheHeaders.hasHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY)) { return emptySet() } return try { @@ -102,7 +108,7 @@ class SqlNormalizedCache internal constructor( @ApolloExperimental override fun merge(records: Collection, cacheHeaders: CacheHeaders, recordMerger: RecordMerger): Set { - if (cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE)) { + if (cacheHeaders.hasHeader(ApolloCacheHeaders.DO_NOT_STORE) || cacheHeaders.hasHeader(ApolloCacheHeaders.MEMORY_CACHE_ONLY)) { return emptySet() } return try { diff --git a/tests/normalized-cache/build.gradle.kts b/tests/normalized-cache/build.gradle.kts new file mode 100644 index 00000000..f662eb8e --- /dev/null +++ b/tests/normalized-cache/build.gradle.kts @@ -0,0 +1,69 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.apollographql.apollo") +} + +kotlin { + jvm() + macosX64() + macosArm64() + iosArm64() + iosX64() + iosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosSimulatorArm64() + tvosArm64() + tvosX64() + tvosSimulatorArm64() + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + group("common") { + group("concurrent") { + group("native") { + group("apple") + } + group("jvmCommon") { + withJvm() + } + } + } + } + + sourceSets { + getByName("commonMain") { + dependencies { + implementation(libs.apollo.runtime) + } + } + + getByName("commonTest") { + dependencies { + implementation(libs.apollo.testing.support) + implementation(libs.apollo.mockserver) + implementation(libs.kotlin.test) + implementation("com.apollographql.cache:normalized-cache-sqlite-incubating") + } + } + + getByName("jvmTest") { + dependencies { + implementation(libs.slf4j.nop) + } + } + + configureEach { + languageSettings.optIn("com.apollographql.apollo.annotations.ApolloExperimental") + languageSettings.optIn("com.apollographql.apollo.annotations.ApolloInternal") + } + } +} + +apollo { + service("service") { + packageName.set("sqlite") + } +} diff --git a/tests/normalized-cache/src/commonMain/graphql/operations.graphql b/tests/normalized-cache/src/commonMain/graphql/operations.graphql new file mode 100644 index 00000000..79ac2378 --- /dev/null +++ b/tests/normalized-cache/src/commonMain/graphql/operations.graphql @@ -0,0 +1,6 @@ +query GetUser { + user { + name + email + } +} \ No newline at end of file diff --git a/tests/normalized-cache/src/commonMain/graphql/schema.graphqls b/tests/normalized-cache/src/commonMain/graphql/schema.graphqls new file mode 100644 index 00000000..3ada9e89 --- /dev/null +++ b/tests/normalized-cache/src/commonMain/graphql/schema.graphqls @@ -0,0 +1,9 @@ +type Query { + user: User +} + +type User { + name: String! + email: String! + admin: Boolean +} \ No newline at end of file diff --git a/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt b/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt new file mode 100644 index 00000000..2885c36a --- /dev/null +++ b/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt @@ -0,0 +1,50 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.exception.CacheMissException +import com.apollographql.apollo.testing.QueueTestNetworkTransport +import com.apollographql.apollo.testing.enqueueTestResponse +import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.cache.normalized.ApolloStore +import com.apollographql.cache.normalized.FetchPolicy +import com.apollographql.cache.normalized.api.MemoryCache +import com.apollographql.cache.normalized.api.MemoryCacheFactory +import com.apollographql.cache.normalized.api.Record +import com.apollographql.cache.normalized.fetchPolicy +import com.apollographql.cache.normalized.memoryCacheOnly +import com.apollographql.cache.normalized.sql.SqlNormalizedCache +import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory +import com.apollographql.cache.normalized.store +import sqlite.GetUserQuery +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class MemoryCacheOnlyTest { + @Test + fun memoryCacheOnlyDoesNotStoreInSqlCache() = runTest { + val store = ApolloStore(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())).also { it.clearAll() } + val apolloClient = ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).store(store).build() + val query = GetUserQuery() + apolloClient.enqueueTestResponse(query, GetUserQuery.Data(GetUserQuery.User("John", "a@a.com"))) + apolloClient.query(query).memoryCacheOnly(true).execute() + val dump: Map, Map> = store.dump() + assertEquals(2, dump[MemoryCache::class]!!.size) + assertEquals(0, dump[SqlNormalizedCache::class]!!.size) + } + + @Test + fun memoryCacheOnlyDoesNotReadFromSqlCache() = runTest { + val store = ApolloStore(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())).also { it.clearAll() } + val query = GetUserQuery() + store.writeOperation(query, GetUserQuery.Data(GetUserQuery.User("John", "a@a.com"))) + + val store2 = ApolloStore(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + val apolloClient = ApolloClient.Builder().serverUrl("unused").store(store2).build() + // The record in is in the SQL cache, but we request not to access it + assertIs( + apolloClient.query(query).fetchPolicy(FetchPolicy.CacheOnly).memoryCacheOnly(true).execute().exception + ) + } +} diff --git a/tests/pagination/build.gradle.kts b/tests/pagination/build.gradle.kts index fa7ffa49..c6deb8cb 100644 --- a/tests/pagination/build.gradle.kts +++ b/tests/pagination/build.gradle.kts @@ -111,8 +111,4 @@ apollo { @OptIn(ApolloExperimental::class) generateDataBuilders.set(true) } - - // Shouldn't be needed after https://github.com/apollographql/apollo-kotlin/blob/e6dfb1a0ba963080b088660ed80691b91b66e54d/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt#L279 - // is released in the Apollo Gradle plugin. - linkSqlite.set(true) } diff --git a/tests/sqlite/build.gradle.kts b/tests/sqlite/build.gradle.kts index 23418d2b..f662eb8e 100644 --- a/tests/sqlite/build.gradle.kts +++ b/tests/sqlite/build.gradle.kts @@ -66,8 +66,4 @@ apollo { service("service") { packageName.set("sqlite") } - - // Shouldn't be needed after https://github.com/apollographql/apollo-kotlin/blob/e6dfb1a0ba963080b088660ed80691b91b66e54d/libraries/apollo-gradle-plugin-external/src/main/kotlin/com/apollographql/apollo3/gradle/internal/DefaultApolloExtension.kt#L279 - // is released in the Apollo Gradle plugin. - linkSqlite.set(true) }