diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.api b/normalized-cache-incubating/api/normalized-cache-incubating.api index 53a1f56..0a3c511 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.api @@ -113,6 +113,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 maxStale-HG0u8IE (Lcom/apollographql/apollo/api/MutableExecutionOptions;J)Ljava/lang/Object; 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; @@ -270,8 +271,8 @@ public final class com/apollographql/cache/normalized/api/EmptyMetadataGenerator public fun metadataForObject (Ljava/lang/Object;Lcom/apollographql/cache/normalized/api/MetadataGeneratorContext;)Ljava/util/Map; } -public final class com/apollographql/cache/normalized/api/ExpireDateCacheResolver : com/apollographql/cache/normalized/api/CacheResolver { - public fun ()V +public final class com/apollographql/cache/normalized/api/ExpirationCacheResolver : com/apollographql/cache/normalized/api/CacheResolver { + public fun (Lcom/apollographql/cache/normalized/api/MaxAgeProvider;)V public fun resolveField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Ljava/lang/Object; } @@ -313,6 +314,35 @@ public abstract interface class com/apollographql/cache/normalized/api/FieldReco public abstract fun mergeFields (Lcom/apollographql/cache/normalized/api/FieldRecordMerger$FieldInfo;Lcom/apollographql/cache/normalized/api/FieldRecordMerger$FieldInfo;)Lcom/apollographql/cache/normalized/api/FieldRecordMerger$FieldInfo; } +public final class com/apollographql/cache/normalized/api/GlobalMaxAgeProvider : com/apollographql/cache/normalized/api/MaxAgeProvider { + public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getMaxAge-5sfh64U (Lcom/apollographql/cache/normalized/api/MaxAgeContext;)J +} + +public abstract interface class com/apollographql/cache/normalized/api/MaxAge { +} + +public final class com/apollographql/cache/normalized/api/MaxAge$Duration : com/apollographql/cache/normalized/api/MaxAge { + public synthetic fun (JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDuration-UwyO8pc ()J +} + +public final class com/apollographql/cache/normalized/api/MaxAge$Inherit : com/apollographql/cache/normalized/api/MaxAge { + public static final field INSTANCE Lcom/apollographql/cache/normalized/api/MaxAge$Inherit; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/apollographql/cache/normalized/api/MaxAgeContext { + public fun (Ljava/util/List;)V + public final fun getFieldPath ()Ljava/util/List; +} + +public abstract interface class com/apollographql/cache/normalized/api/MaxAgeProvider { + public abstract fun getMaxAge-5sfh64U (Lcom/apollographql/cache/normalized/api/MaxAgeContext;)J +} + public final class com/apollographql/cache/normalized/api/MemoryCache : com/apollographql/cache/normalized/api/NormalizedCache { public fun ()V public fun (Lcom/apollographql/cache/normalized/api/NormalizedCache;IJ)V @@ -391,11 +421,6 @@ public abstract interface class com/apollographql/cache/normalized/api/ReadOnlyN public abstract fun loadRecords (Ljava/util/Collection;Lcom/apollographql/cache/normalized/api/CacheHeaders;)Ljava/util/Collection; } -public final class com/apollographql/cache/normalized/api/ReceiveDateCacheResolver : com/apollographql/cache/normalized/api/CacheResolver { - public fun (I)V - public fun resolveField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Ljava/lang/Object; -} - public final class com/apollographql/cache/normalized/api/Record : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { public static final field Companion Lcom/apollographql/cache/normalized/api/Record$Companion; public fun (Ljava/lang/String;Ljava/util/Map;Ljava/util/UUID;)V @@ -464,16 +489,22 @@ public final class com/apollographql/cache/normalized/api/RecordMergerKt { } public final class com/apollographql/cache/normalized/api/ResolverContext { - public fun (Lcom/apollographql/apollo/api/CompiledField;Lcom/apollographql/apollo/api/Executable$Variables;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Lcom/apollographql/cache/normalized/api/CacheHeaders;Lcom/apollographql/cache/normalized/api/FieldKeyGenerator;)V + public fun (Lcom/apollographql/apollo/api/CompiledField;Lcom/apollographql/apollo/api/Executable$Variables;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Lcom/apollographql/cache/normalized/api/CacheHeaders;Lcom/apollographql/cache/normalized/api/FieldKeyGenerator;Ljava/util/List;)V public final fun getCacheHeaders ()Lcom/apollographql/cache/normalized/api/CacheHeaders; public final fun getField ()Lcom/apollographql/apollo/api/CompiledField; public final fun getFieldKeyGenerator ()Lcom/apollographql/cache/normalized/api/FieldKeyGenerator; public final fun getParent ()Ljava/util/Map; public final fun getParentKey ()Ljava/lang/String; public final fun getParentType ()Ljava/lang/String; + public final fun getPath ()Ljava/util/List; public final fun getVariables ()Lcom/apollographql/apollo/api/Executable$Variables; } +public final class com/apollographql/cache/normalized/api/SchemaCoordinatesMaxAgeProvider : com/apollographql/cache/normalized/api/MaxAgeProvider { + public synthetic fun (Ljava/util/Map;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getMaxAge-5sfh64U (Lcom/apollographql/cache/normalized/api/MaxAgeContext;)J +} + public final class com/apollographql/cache/normalized/api/TypePolicyCacheKeyGenerator : com/apollographql/cache/normalized/api/CacheKeyGenerator { public static final field INSTANCE Lcom/apollographql/cache/normalized/api/TypePolicyCacheKeyGenerator; public fun cacheKeyForObject (Ljava/util/Map;Lcom/apollographql/cache/normalized/api/CacheKeyGeneratorContext;)Lcom/apollographql/cache/normalized/api/CacheKey; diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api index 606343d..503b9bc 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api @@ -43,6 +43,9 @@ abstract interface com.apollographql.cache.normalized.api/EmbeddedFieldsProvider abstract interface com.apollographql.cache.normalized.api/FieldKeyGenerator { // com.apollographql.cache.normalized.api/FieldKeyGenerator|null[0] abstract fun getFieldKey(com.apollographql.cache.normalized.api/FieldKeyContext): kotlin/String // com.apollographql.cache.normalized.api/FieldKeyGenerator.getFieldKey|getFieldKey(com.apollographql.cache.normalized.api.FieldKeyContext){}[0] } +abstract interface com.apollographql.cache.normalized.api/MaxAgeProvider { // com.apollographql.cache.normalized.api/MaxAgeProvider|null[0] + abstract fun getMaxAge(com.apollographql.cache.normalized.api/MaxAgeContext): kotlin.time/Duration // com.apollographql.cache.normalized.api/MaxAgeProvider.getMaxAge|getMaxAge(com.apollographql.cache.normalized.api.MaxAgeContext){}[0] +} abstract interface com.apollographql.cache.normalized.api/MetadataGenerator { // com.apollographql.cache.normalized.api/MetadataGenerator|null[0] abstract fun metadataForObject(kotlin/Any?, com.apollographql.cache.normalized.api/MetadataGeneratorContext): kotlin.collections/Map // com.apollographql.cache.normalized.api/MetadataGenerator.metadataForObject|metadataForObject(kotlin.Any?;com.apollographql.cache.normalized.api.MetadataGeneratorContext){}[0] } @@ -173,9 +176,9 @@ final class com.apollographql.cache.normalized.api/EmbeddedFieldsContext { // co final val parentType // com.apollographql.cache.normalized.api/EmbeddedFieldsContext.parentType|{}parentType[0] final fun (): com.apollographql.apollo.api/CompiledNamedType // com.apollographql.cache.normalized.api/EmbeddedFieldsContext.parentType.|(){}[0] } -final class com.apollographql.cache.normalized.api/ExpireDateCacheResolver : com.apollographql.cache.normalized.api/CacheResolver { // com.apollographql.cache.normalized.api/ExpireDateCacheResolver|null[0] - constructor () // com.apollographql.cache.normalized.api/ExpireDateCacheResolver.|(){}[0] - final fun resolveField(com.apollographql.cache.normalized.api/ResolverContext): kotlin/Any? // com.apollographql.cache.normalized.api/ExpireDateCacheResolver.resolveField|resolveField(com.apollographql.cache.normalized.api.ResolverContext){}[0] +final class com.apollographql.cache.normalized.api/ExpirationCacheResolver : com.apollographql.cache.normalized.api/CacheResolver { // com.apollographql.cache.normalized.api/ExpirationCacheResolver|null[0] + constructor (com.apollographql.cache.normalized.api/MaxAgeProvider) // com.apollographql.cache.normalized.api/ExpirationCacheResolver.|(com.apollographql.cache.normalized.api.MaxAgeProvider){}[0] + final fun resolveField(com.apollographql.cache.normalized.api/ResolverContext): kotlin/Any? // com.apollographql.cache.normalized.api/ExpirationCacheResolver.resolveField|resolveField(com.apollographql.cache.normalized.api.ResolverContext){}[0] } final class com.apollographql.cache.normalized.api/FieldKeyContext { // com.apollographql.cache.normalized.api/FieldKeyContext|null[0] constructor (kotlin/String, com.apollographql.apollo.api/CompiledField, com.apollographql.apollo.api/Executable.Variables) // com.apollographql.cache.normalized.api/FieldKeyContext.|(kotlin.String;com.apollographql.apollo.api.CompiledField;com.apollographql.apollo.api.Executable.Variables){}[0] @@ -206,6 +209,15 @@ final class com.apollographql.cache.normalized.api/FieldRecordMerger : com.apoll } final fun merge(com.apollographql.cache.normalized.api/Record, com.apollographql.cache.normalized.api/Record): kotlin/Pair> // com.apollographql.cache.normalized.api/FieldRecordMerger.merge|merge(com.apollographql.cache.normalized.api.Record;com.apollographql.cache.normalized.api.Record){}[0] } +final class com.apollographql.cache.normalized.api/GlobalMaxAgeProvider : com.apollographql.cache.normalized.api/MaxAgeProvider { // com.apollographql.cache.normalized.api/GlobalMaxAgeProvider|null[0] + constructor (kotlin.time/Duration) // com.apollographql.cache.normalized.api/GlobalMaxAgeProvider.|(kotlin.time.Duration){}[0] + final fun getMaxAge(com.apollographql.cache.normalized.api/MaxAgeContext): kotlin.time/Duration // com.apollographql.cache.normalized.api/GlobalMaxAgeProvider.getMaxAge|getMaxAge(com.apollographql.cache.normalized.api.MaxAgeContext){}[0] +} +final class com.apollographql.cache.normalized.api/MaxAgeContext { // com.apollographql.cache.normalized.api/MaxAgeContext|null[0] + constructor (kotlin.collections/List) // com.apollographql.cache.normalized.api/MaxAgeContext.|(kotlin.collections.List){}[0] + final val fieldPath // com.apollographql.cache.normalized.api/MaxAgeContext.fieldPath|{}fieldPath[0] + final fun (): kotlin.collections/List // com.apollographql.cache.normalized.api/MaxAgeContext.fieldPath.|(){}[0] +} final class com.apollographql.cache.normalized.api/MemoryCache : com.apollographql.cache.normalized.api/NormalizedCache { // com.apollographql.cache.normalized.api/MemoryCache|null[0] constructor (com.apollographql.cache.normalized.api/NormalizedCache? = ..., kotlin/Int = ..., kotlin/Long = ...) // com.apollographql.cache.normalized.api/MemoryCache.|(com.apollographql.cache.normalized.api.NormalizedCache?;kotlin.Int;kotlin.Long){}[0] final fun clearAll() // com.apollographql.cache.normalized.api/MemoryCache.clearAll|clearAll(){}[0] @@ -233,10 +245,6 @@ final class com.apollographql.cache.normalized.api/MetadataGeneratorContext { // final val variables // com.apollographql.cache.normalized.api/MetadataGeneratorContext.variables|{}variables[0] final fun (): com.apollographql.apollo.api/Executable.Variables // com.apollographql.cache.normalized.api/MetadataGeneratorContext.variables.|(){}[0] } -final class com.apollographql.cache.normalized.api/ReceiveDateCacheResolver : com.apollographql.cache.normalized.api/CacheResolver { // com.apollographql.cache.normalized.api/ReceiveDateCacheResolver|null[0] - constructor (kotlin/Int) // com.apollographql.cache.normalized.api/ReceiveDateCacheResolver.|(kotlin.Int){}[0] - final fun resolveField(com.apollographql.cache.normalized.api/ResolverContext): kotlin/Any? // com.apollographql.cache.normalized.api/ReceiveDateCacheResolver.resolveField|resolveField(com.apollographql.cache.normalized.api.ResolverContext){}[0] -} final class com.apollographql.cache.normalized.api/Record : kotlin.collections/Map { // com.apollographql.cache.normalized.api/Record|null[0] constructor (kotlin/String, kotlin.collections/Map, com.benasher44.uuid/Uuid? = ...) // com.apollographql.cache.normalized.api/Record.|(kotlin.String;kotlin.collections.Map;com.benasher44.uuid.Uuid?){}[0] constructor (kotlin/String, kotlin.collections/Map, com.benasher44.uuid/Uuid?, kotlin.collections/Map>) // com.apollographql.cache.normalized.api/Record.|(kotlin.String;kotlin.collections.Map;com.benasher44.uuid.Uuid?;kotlin.collections.Map>){}[0] @@ -270,7 +278,7 @@ final class com.apollographql.cache.normalized.api/Record : kotlin.collections/M final fun asJsReadonlyMapView(): kotlin.js.collections/JsReadonlyMap // com.apollographql.cache.normalized.api/Record.asJsReadonlyMapView|asJsReadonlyMapView(){}[0] } final class com.apollographql.cache.normalized.api/ResolverContext { // com.apollographql.cache.normalized.api/ResolverContext|null[0] - constructor (com.apollographql.apollo.api/CompiledField, com.apollographql.apollo.api/Executable.Variables, kotlin.collections/Map, kotlin/String, kotlin/String, com.apollographql.cache.normalized.api/CacheHeaders, com.apollographql.cache.normalized.api/FieldKeyGenerator) // com.apollographql.cache.normalized.api/ResolverContext.|(com.apollographql.apollo.api.CompiledField;com.apollographql.apollo.api.Executable.Variables;kotlin.collections.Map;kotlin.String;kotlin.String;com.apollographql.cache.normalized.api.CacheHeaders;com.apollographql.cache.normalized.api.FieldKeyGenerator){}[0] + constructor (com.apollographql.apollo.api/CompiledField, com.apollographql.apollo.api/Executable.Variables, kotlin.collections/Map, kotlin/String, kotlin/String, com.apollographql.cache.normalized.api/CacheHeaders, com.apollographql.cache.normalized.api/FieldKeyGenerator, kotlin.collections/List) // com.apollographql.cache.normalized.api/ResolverContext.|(com.apollographql.apollo.api.CompiledField;com.apollographql.apollo.api.Executable.Variables;kotlin.collections.Map;kotlin.String;kotlin.String;com.apollographql.cache.normalized.api.CacheHeaders;com.apollographql.cache.normalized.api.FieldKeyGenerator;kotlin.collections.List){}[0] final val cacheHeaders // com.apollographql.cache.normalized.api/ResolverContext.cacheHeaders|{}cacheHeaders[0] final fun (): com.apollographql.cache.normalized.api/CacheHeaders // com.apollographql.cache.normalized.api/ResolverContext.cacheHeaders.|(){}[0] final val field // com.apollographql.cache.normalized.api/ResolverContext.field|{}field[0] @@ -283,9 +291,15 @@ final class com.apollographql.cache.normalized.api/ResolverContext { // com.apol final fun (): kotlin/String // com.apollographql.cache.normalized.api/ResolverContext.parentKey.|(){}[0] final val parentType // com.apollographql.cache.normalized.api/ResolverContext.parentType|{}parentType[0] final fun (): kotlin/String // com.apollographql.cache.normalized.api/ResolverContext.parentType.|(){}[0] + final val path // com.apollographql.cache.normalized.api/ResolverContext.path|{}path[0] + final fun (): kotlin.collections/List // com.apollographql.cache.normalized.api/ResolverContext.path.|(){}[0] final val variables // com.apollographql.cache.normalized.api/ResolverContext.variables|{}variables[0] final fun (): com.apollographql.apollo.api/Executable.Variables // com.apollographql.cache.normalized.api/ResolverContext.variables.|(){}[0] } +final class com.apollographql.cache.normalized.api/SchemaCoordinatesMaxAgeProvider : com.apollographql.cache.normalized.api/MaxAgeProvider { // com.apollographql.cache.normalized.api/SchemaCoordinatesMaxAgeProvider|null[0] + constructor (kotlin.collections/Map, kotlin.time/Duration) // com.apollographql.cache.normalized.api/SchemaCoordinatesMaxAgeProvider.|(kotlin.collections.Map;kotlin.time.Duration){}[0] + final fun getMaxAge(com.apollographql.cache.normalized.api/MaxAgeContext): kotlin.time/Duration // com.apollographql.cache.normalized.api/SchemaCoordinatesMaxAgeProvider.getMaxAge|getMaxAge(com.apollographql.cache.normalized.api.MaxAgeContext){}[0] +} final class com.apollographql.cache.normalized/CacheInfo : com.apollographql.apollo.api/ExecutionContext.Element { // com.apollographql.cache.normalized/CacheInfo|null[0] constructor (kotlin/Long, kotlin/Long, kotlin/Boolean, kotlin/String?, kotlin/String?) // com.apollographql.cache.normalized/CacheInfo.|(kotlin.Long;kotlin.Long;kotlin.Boolean;kotlin.String?;kotlin.String?){}[0] final class Builder { // com.apollographql.cache.normalized/CacheInfo.Builder|null[0] @@ -371,6 +385,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/maxStale(kotlin.time/Duration): #A // com.apollographql.cache.normalized/maxStale|maxStale@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(kotlin.time.Duration){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] @@ -438,3 +453,15 @@ final val com.apollographql.cache.normalized/cacheInfo // com.apollographql.cach final fun <#A1: com.apollographql.apollo.api/Operation.Data> (com.apollographql.apollo.api/ApolloResponse<#A1>).(): com.apollographql.cache.normalized/CacheInfo? // com.apollographql.cache.normalized/cacheInfo.|@com.apollographql.apollo.api.ApolloResponse<0:0>(){0§}[0] final val com.apollographql.cache.normalized/isFromCache // com.apollographql.cache.normalized/isFromCache|@com.apollographql.apollo.api.ApolloResponse<0:0>{0§}isFromCache[0] final fun <#A1: com.apollographql.apollo.api/Operation.Data> (com.apollographql.apollo.api/ApolloResponse<#A1>).(): kotlin/Boolean // com.apollographql.cache.normalized/isFromCache.|@com.apollographql.apollo.api.ApolloResponse<0:0>(){0§}[0] +sealed interface com.apollographql.cache.normalized.api/MaxAge { // com.apollographql.cache.normalized.api/MaxAge|null[0] + final class Duration : com.apollographql.cache.normalized.api/MaxAge { // com.apollographql.cache.normalized.api/MaxAge.Duration|null[0] + constructor (kotlin.time/Duration) // com.apollographql.cache.normalized.api/MaxAge.Duration.|(kotlin.time.Duration){}[0] + final val duration // com.apollographql.cache.normalized.api/MaxAge.Duration.duration|{}duration[0] + final fun (): kotlin.time/Duration // com.apollographql.cache.normalized.api/MaxAge.Duration.duration.|(){}[0] + } + final object Inherit : com.apollographql.cache.normalized.api/MaxAge { // com.apollographql.cache.normalized.api/MaxAge.Inherit|null[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.apollographql.cache.normalized.api/MaxAge.Inherit.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.apollographql.cache.normalized.api/MaxAge.Inherit.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.apollographql.cache.normalized.api/MaxAge.Inherit.toString|toString(){}[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 bc28ba7..f4c8d53 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 @@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlin.jvm.JvmName import kotlin.jvm.JvmOverloads +import kotlin.time.Duration enum class FetchPolicy { /** @@ -401,6 +402,13 @@ fun MutableExecutionOptions.cacheHeaders(cacheHeaders: CacheHeaders) = ad CacheHeadersContext(cacheHeaders) ) +/** + * @param maxStale how long to accept stale fields + */ +fun MutableExecutionOptions.maxStale(maxStale: Duration) = cacheHeaders( + CacheHeaders.Builder().addHeader(ApolloCacheHeaders.MAX_STALE, maxStale.inWholeSeconds.toString()).build() +) + /** * @param writeToCacheAsynchronously whether to return the response before writing it to the cache * @@ -698,6 +706,10 @@ val ApolloResponse.cacheHeaders * * Any [FetchPolicy] previously set will be ignored */ -@Deprecated("Use fetchPolicy(FetchPolicy.CacheAndNetwork) instead", ReplaceWith("fetchPolicy(FetchPolicy.CacheAndNetwork).toFlow()"), level = DeprecationLevel.ERROR) +@Deprecated( + "Use fetchPolicy(FetchPolicy.CacheAndNetwork) instead", + ReplaceWith("fetchPolicy(FetchPolicy.CacheAndNetwork).toFlow()"), + level = DeprecationLevel.ERROR +) @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v3_7_5) fun ApolloCall.executeCacheAndNetwork(): Flow> = TODO() diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt index ffbfc8c..a7c7a6d 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt @@ -2,8 +2,12 @@ package com.apollographql.cache.normalized.api import com.apollographql.apollo.api.CompiledField import com.apollographql.apollo.api.Executable +import com.apollographql.apollo.api.MutableExecutionOptions import com.apollographql.apollo.exception.CacheMissException import com.apollographql.apollo.mpp.currentTimeMillis +import com.apollographql.cache.normalized.maxStale +import com.apollographql.cache.normalized.storeExpirationDate +import com.apollographql.cache.normalized.storeReceiveDate import kotlin.jvm.JvmSuppressWildcards /** @@ -104,6 +108,12 @@ class ResolverContext( * The [FieldKeyGenerator] to use to generate field keys */ val fieldKeyGenerator: FieldKeyGenerator, + + /** + * The path of the field to resolve. + * The first element is the root object, the last element is [field]. + */ + val path: List, ) /** @@ -121,58 +131,59 @@ object DefaultCacheResolver : CacheResolver { } /** - * A cache resolver that uses the cache date as a receive date and expires after a fixed max age + * A cache resolver that raises a cache miss if the field's received date is older than its max age + * (configurable via [maxAgeProvider]) or its expiration date has passed. + * + * Received dates are stored by calling `storeReceiveDate(true)` on your `ApolloClient`. + * + * Expiration dates are stored by calling `storeExpirationDate(true)` on your `ApolloClient`. + * + * A maximum staleness can be configured via the [ApolloCacheHeaders.MAX_STALE] cache header. + * + * @see MutableExecutionOptions.storeReceiveDate + * @see MutableExecutionOptions.storeExpirationDate + * @see MutableExecutionOptions.maxStale */ -class ReceiveDateCacheResolver(private val maxAge: Int) : CacheResolver { +class ExpirationCacheResolver( + private val maxAgeProvider: MaxAgeProvider, +) : CacheResolver { override fun resolveField(context: ResolverContext): Any? { - val parent = context.parent - val parentKey = context.parentKey - - val fieldKey = context.fieldKeyGenerator.getFieldKey(FieldKeyContext(context.parentType, context.field, context.variables)) - if (!parent.containsKey(fieldKey)) { - throw CacheMissException(parentKey, fieldKey) - } - - if (parent is Record) { - val receivedDate = parent.receivedDate(fieldKey) - if (receivedDate != null) { - val maxStale = context.cacheHeaders.headerValue(ApolloCacheHeaders.MAX_STALE)?.toLongOrNull() ?: 0L - if (maxStale < Long.MAX_VALUE) { - val age = currentTimeMillis() / 1000 - receivedDate - if (maxAge + maxStale - age < 0) { - throw CacheMissException(parentKey, fieldKey, true) - } + val resolvedField = FieldPolicyCacheResolver.resolveField(context) + if (context.parent is Record) { + val field = context.field + val maxStale = context.cacheHeaders.headerValue(ApolloCacheHeaders.MAX_STALE)?.toLongOrNull() ?: 0L + val currentDate = currentTimeMillis() / 1000 + + // Consider the field's max age (client side) + val fieldMaxAge = maxAgeProvider.getMaxAge(MaxAgeContext(context.path)).inWholeSeconds + val fieldReceivedDate = context.parent.receivedDate(field.name) + if (fieldReceivedDate != null) { + val fieldAge = currentDate - fieldReceivedDate + val stale = fieldAge - fieldMaxAge + if (stale >= maxStale) { + throw CacheMissException( + context.parentKey, + context.fieldKeyGenerator.getFieldKey(FieldKeyContext(context.parentType, context.field, context.variables)), + true + ) } } - } - - return parent[fieldKey] - } -} - -/** - * A cache resolver that uses the cache date as an expiration date and expires past it - */ -class ExpireDateCacheResolver : CacheResolver { - override fun resolveField(context: ResolverContext): Any? { - val parent = context.parent - val parentKey = context.parentKey - - val fieldKey = context.fieldKeyGenerator.getFieldKey(FieldKeyContext(context.parentType, context.field, context.variables)) - if (!parent.containsKey(fieldKey)) { - throw CacheMissException(parentKey, fieldKey) - } - if (parent is Record) { - val expirationDate = parent.expirationDate(fieldKey) - if (expirationDate != null) { - if (currentTimeMillis() / 1000 - expirationDate >= 0) { - throw CacheMissException(parentKey, fieldKey, true) + // Consider the field's expiration date (server side) + val fieldExpirationDate = context.parent.expirationDate(field.name) + if (fieldExpirationDate != null) { + val stale = currentDate - fieldExpirationDate + if (stale >= maxStale) { + throw CacheMissException( + context.parentKey, + context.fieldKeyGenerator.getFieldKey(FieldKeyContext(context.parentType, context.field, context.variables)), + true + ) } } } - return parent[fieldKey] + return resolvedField } } diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt new file mode 100644 index 0000000..0d91583 --- /dev/null +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/MaxAgeProvider.kt @@ -0,0 +1,115 @@ +package com.apollographql.cache.normalized.api + +import com.apollographql.apollo.api.CompiledField +import com.apollographql.apollo.api.isComposite +import kotlin.time.Duration + +interface MaxAgeProvider { + /** + * Returns the max age for the given field. + */ + fun getMaxAge(maxAgeContext: MaxAgeContext): Duration +} + +class MaxAgeContext( + /** + * The path of the field to get the max age of. + * The first element is the root object, the last element is the field to get the max age of. + */ + val fieldPath: List, +) + +/** + * A provider that returns a single max age for all types. + */ +class GlobalMaxAgeProvider(private val maxAge: Duration) : MaxAgeProvider { + override fun getMaxAge(maxAgeContext: MaxAgeContext): Duration = maxAge +} + +sealed interface MaxAge { + class Duration(val duration: kotlin.time.Duration) : MaxAge + data object Inherit : MaxAge +} + +/** + * A provider that returns a max age based on [schema coordinates](https://github.com/graphql/graphql-spec/pull/794). + * The given coordinates must be object/interface/union (e.g. `MyType`) or field (e.g. `MyType.myField`) coordinates. + * + * The max age of a field is determined as follows: + * - If the field has a [MaxAge.Duration] max age, return it. + * - Else, if the field has a [MaxAge.Inherit] max age, return the max age of the parent field. + * - Else, if the field's type has a [MaxAge.Duration] max age, return it. + * - Else, if the field's type has a [MaxAge.Inherit] max age, return the max age of the parent field. + * - Else, if the field is a root field, or the field's type is composite, return the default max age. + * - Else, return the max age of the parent field. + * + * Then the lowest of the field's max age and its parent field's max age is returned. + */ +class SchemaCoordinatesMaxAgeProvider( + private val coordinatesToMaxAges: Map, + private val defaultMaxAge: Duration, +) : MaxAgeProvider { + override fun getMaxAge(maxAgeContext: MaxAgeContext): Duration { + if (maxAgeContext.fieldPath.size == 1) { + // Root field + return defaultMaxAge + } + + val fieldName = maxAgeContext.fieldPath.last().name + val fieldParentTypeName = maxAgeContext.fieldPath[maxAgeContext.fieldPath.lastIndex - 1].type.rawType().name + val fieldCoordinates = "$fieldParentTypeName.$fieldName" + val computedFieldMaxAge = when (val fieldMaxAge = coordinatesToMaxAges[fieldCoordinates]) { + is MaxAge.Duration -> { + fieldMaxAge.duration + } + + is MaxAge.Inherit -> { + getParentMaxAge(maxAgeContext) + } + + null -> { + getTypeMaxAge(maxAgeContext) + } + } + val isRootField = maxAgeContext.fieldPath.size == 2 + return if (isRootField) { + computedFieldMaxAge + } else { + minOf(computedFieldMaxAge, getParentMaxAge(maxAgeContext)) + } + } + + private fun getParentMaxAge(maxAgeContext: MaxAgeContext): Duration = getMaxAge(MaxAgeContext(maxAgeContext.fieldPath.dropLast(1))) + + private fun getTypeMaxAge(maxAgeContext: MaxAgeContext): Duration { + val field = maxAgeContext.fieldPath.last() + val fieldTypeName = field.type.rawType().name + return when (val typeMaxAge = coordinatesToMaxAges[fieldTypeName]) { + is MaxAge.Duration -> { + typeMaxAge.duration + } + + is MaxAge.Inherit -> { + getParentMaxAge(maxAgeContext) + } + + null -> { + getFallbackMaxAge(maxAgeContext) + } + } + } + + // Fallback: + // - root fields have the default maxAge + // - same for fields that return a composite type + // - non root fields that return a leaf type inherit the maxAge of their parent field + private fun getFallbackMaxAge(maxAgeContext: MaxAgeContext): Duration { + val field = maxAgeContext.fieldPath.last() + val isRootField = maxAgeContext.fieldPath.size == 2 + return if (isRootField || field.type.rawType().isComposite()) { + defaultMaxAge + } else { + getParentMaxAge(maxAgeContext) + } + } +} diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/OperationCacheExtensions.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/OperationCacheExtensions.kt index f9124fd..e990ec3 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/OperationCacheExtensions.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/OperationCacheExtensions.kt @@ -107,7 +107,7 @@ private fun Executable.readInternal( variables = variables, rootKey = cacheKey.key, rootSelections = rootField().selections, - rootTypename = rootField().type.rawType().name, + rootField = rootField(), fieldKeyGenerator = fieldKeyGenerator, ).collectData() } diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/internal/CacheBatchReader.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/internal/CacheBatchReader.kt index eced495..f0fd3b1 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/internal/CacheBatchReader.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/internal/CacheBatchReader.kt @@ -28,7 +28,7 @@ internal class CacheBatchReader( private val cacheResolver: CacheResolver, private val cacheHeaders: CacheHeaders, private val rootSelections: List, - private val rootTypename: String, + private val rootField: CompiledField, private val fieldKeyGenerator: FieldKeyGenerator, ) { /** @@ -38,6 +38,7 @@ internal class CacheBatchReader( class PendingReference( val key: String, val path: List, + val fieldPath: List, val selections: List, val parentType: String, ) @@ -91,8 +92,9 @@ internal class CacheBatchReader( PendingReference( key = rootKey, selections = rootSelections, - parentType = rootTypename, - path = emptyList() + parentType = rootField.type.rawType().name, + path = emptyList(), + fieldPath = listOf(rootField), ) ) @@ -129,9 +131,10 @@ internal class CacheBatchReader( parentType = pendingReference.parentType, cacheHeaders = cacheHeaders, fieldKeyGenerator = fieldKeyGenerator, + path = pendingReference.fieldPath + it, ) ) - value.registerCacheKeys(pendingReference.path + it.responseName, it.selections, it.type.rawType().name) + value.registerCacheKeys(pendingReference.path + it.responseName, pendingReference.fieldPath + it, it.selections, it.type.rawType().name) it.responseName to value }.toMap() @@ -146,7 +149,12 @@ internal class CacheBatchReader( /** * The path leading to this value */ - private fun Any?.registerCacheKeys(path: List, selections: List, parentType: String) { + private fun Any?.registerCacheKeys( + path: List, + fieldPath: List, + selections: List, + parentType: String, + ) { when (this) { is CacheKey -> { pendingReferences.add( @@ -154,14 +162,15 @@ internal class CacheBatchReader( key = key, selections = selections, parentType = parentType, - path = path + path = path, + fieldPath = fieldPath, ) ) } is List<*> -> { forEachIndexed { index, value -> - value.registerCacheKeys(path + index, selections, parentType) + value.registerCacheKeys(path + index, fieldPath, selections, parentType) } } @@ -183,9 +192,10 @@ internal class CacheBatchReader( parentType = parentType, cacheHeaders = cacheHeaders, fieldKeyGenerator = fieldKeyGenerator, + path = fieldPath + it, ) ) - value.registerCacheKeys(path + it.responseName, it.selections, it.type.rawType().name) + value.registerCacheKeys(path + it.responseName, fieldPath + it, it.selections, it.type.rawType().name) it.responseName to value }.toMap() diff --git a/normalized-cache-incubating/src/commonTest/kotlin/com/apollographql/cache/normalized/CacheKeyResolverTest.kt b/normalized-cache-incubating/src/commonTest/kotlin/com/apollographql/cache/normalized/CacheKeyResolverTest.kt index 4106e09..e82edcd 100644 --- a/normalized-cache-incubating/src/commonTest/kotlin/com/apollographql/cache/normalized/CacheKeyResolverTest.kt +++ b/normalized-cache-incubating/src/commonTest/kotlin/com/apollographql/cache/normalized/CacheKeyResolverTest.kt @@ -37,7 +37,16 @@ class CacheKeyResolverTest { } private fun resolverContext(field: CompiledField) = - ResolverContext(field, Executable.Variables(emptyMap()), emptyMap(), "", "", CacheHeaders(emptyMap()), DefaultFieldKeyGenerator) + ResolverContext( + field, + Executable.Variables(emptyMap()), + emptyMap(), + "", + "", + CacheHeaders(emptyMap()), + DefaultFieldKeyGenerator, + emptyList() + ) @Test fun verify_cacheKeyForField_called_for_named_composite_field() { diff --git a/tests/expiration/src/commonMain/graphql/operations.graphql b/tests/expiration/src/commonMain/graphql/operations.graphql index 79ac237..0de01a4 100644 --- a/tests/expiration/src/commonMain/graphql/operations.graphql +++ b/tests/expiration/src/commonMain/graphql/operations.graphql @@ -2,5 +2,111 @@ query GetUser { user { name email + admin } -} \ No newline at end of file +} + +query GetUserAdmin { + user { + admin + } +} + +query GetUserEmail { + user { + email + } +} + +query GetUserName { + user { + name + } +} + +query GetCompany { + company { + id + } +} + +# maxAge: 0 +# Query.book doesn't set a maxAge and it's a root field (default 0). +query GetBookTitle { + book { # 0 + cachedTitle # 30 + } +} + +# maxAge: 60 +# Query.cachedBook has a maxAge of 60, and Book.title is a scalar, so it +# inherits maxAge from its parent by default. +query GetCachedBookTitle { + cachedBook { # 60 + title # inherits + } +} + +# maxAge: 30 +# Query.cachedBook has a maxAge of 60, but Book.cachedTitle has +# a maxAge of 30. +query GetCachedBookCachedTitle { + cachedBook { # 60 + cachedTitle # 30 + } +} + +# maxAge: 40 +# Query.reader has a maxAge of 40. Reader.Book is set to +# inheritMaxAge from its parent, and Book.title is a scalar +# that inherits maxAge from its parent by default. +query GetReaderBookTitle { + reader { # 40 + book { # inherits + title # inherits + } + } +} + +query GetProducts { + products { + id + name + price + colors { + ... on StandardColor { + color + } + ... on CustomColor { + red + green + blue + } + } + } + currentUserId +} + +query GetProduct { + product(id: "1") { + id + name + price + colors { + ... on StandardColor { + color + } + ... on CustomColor { + red + green + blue + } + } + } +} + +query GetNodes { + node(id: "1") { + id + } +} diff --git a/tests/expiration/src/commonMain/graphql/schema.graphqls b/tests/expiration/src/commonMain/graphql/schema.graphqls index 3ada9e8..67f65e3 100644 --- a/tests/expiration/src/commonMain/graphql/schema.graphqls +++ b/tests/expiration/src/commonMain/graphql/schema.graphqls @@ -1,9 +1,63 @@ type Query { user: User + company: Company + products: [Product] + product(id: ID!): Product + node(id: ID!): Node + book: Book + cachedBook: Book + reader: Reader + currentUserId: String } type User { name: String! email: String! admin: Boolean -} \ No newline at end of file +} + +type Company { + id: ID! +} + +interface Node { + id: ID! +} + +type Product implements Node { + id: ID! + name: String! + price: Float! + colors: [ProductColor] +} + +union ProductColor = StandardColor | CustomColor + +type StandardColor { + color: Color +} + +enum Color { + BLACK + WHITE + RED + GREEN + BLUE + ORANGE +} + +type CustomColor { + red: Int! + green: Int! + blue: Int! +} + + +type Book { + title: String + cachedTitle: String +} + +type Reader { + book: Book +} diff --git a/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt new file mode 100644 index 0000000..1aa3ce4 --- /dev/null +++ b/tests/expiration/src/commonTest/kotlin/ClientAndServerSideExpirationTest.kt @@ -0,0 +1,106 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.exception.CacheMissException +import com.apollographql.apollo.mpp.currentTimeMillis +import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.cache.normalized.FetchPolicy +import com.apollographql.cache.normalized.api.ExpirationCacheResolver +import com.apollographql.cache.normalized.api.MaxAge +import com.apollographql.cache.normalized.api.MemoryCacheFactory +import com.apollographql.cache.normalized.api.NormalizedCacheFactory +import com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider +import com.apollographql.cache.normalized.apolloStore +import com.apollographql.cache.normalized.cacheHeaders +import com.apollographql.cache.normalized.fetchPolicy +import com.apollographql.cache.normalized.normalizedCache +import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory +import com.apollographql.cache.normalized.storeExpirationDate +import com.apollographql.mockserver.MockResponse +import com.apollographql.mockserver.MockServer +import sqlite.GetUserEmailQuery +import sqlite.GetUserNameQuery +import sqlite.GetUserQuery +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class ClientAndServerSideExpirationTest { + @Test + fun memoryCache() { + test(MemoryCacheFactory()) + } + + @Test + fun sqlCache() { + test(SqlNormalizedCacheFactory()) + } + + @Test + fun chainedCache() { + test(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + } + + private fun test(normalizedCacheFactory: NormalizedCacheFactory) = runTest { + val mockServer = MockServer() + val client = ApolloClient.Builder() + .normalizedCache( + normalizedCacheFactory = normalizedCacheFactory, + cacheResolver = ExpirationCacheResolver( + SchemaCoordinatesMaxAgeProvider( + mapOf( + "User.email" to MaxAge.Duration(2.seconds), + ), + defaultMaxAge = 20.seconds, + ) + ) + ) + .storeExpirationDate(true) + .serverUrl(mockServer.url()) + .build() + client.apolloStore.clearAll() + + val data = """ + { + "data": { + "user": { + "name": "John", + "email": "john@doe.com", + "admin": true + } + } + } + """.trimIndent() + + // Store data with an expiration date 10s in the future, and a received date 10s in the past + mockServer.enqueue( + MockResponse.Builder() + .addHeader("Cache-Control", "max-age=10") + .body(data) + .build() + ) + client.query(GetUserQuery()).fetchPolicy(FetchPolicy.NetworkOnly).cacheHeaders(cacheHeaders(currentTimeMillis() / 1000 - 10)).execute() + + // Read User.name from cache -> it should succeed + val userNameResponse = client.query(GetUserNameQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userNameResponse.data?.user?.name == "John") + + // Read User.email from cache -> it should fail + var userEmailResponse = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + var e = userEmailResponse.exception as CacheMissException + assertTrue(e.stale) + + // Store data with an expired date of now + mockServer.enqueue( + MockResponse.Builder() + .addHeader("Cache-Control", "max-age=0") + .body(data) + .build() + ) + client.query(GetUserQuery()).fetchPolicy(FetchPolicy.NetworkOnly).execute() + // Read User.name from cache -> it should fail + userEmailResponse = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + e = userEmailResponse.exception as CacheMissException + assertTrue(e.stale) + } +} diff --git a/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt index d20bc71..e797d56 100644 --- a/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt +++ b/tests/expiration/src/commonTest/kotlin/ClientSideExpirationTest.kt @@ -9,53 +9,73 @@ import com.apollographql.cache.normalized.FetchPolicy import com.apollographql.cache.normalized.api.ApolloCacheHeaders import com.apollographql.cache.normalized.api.CacheHeaders import com.apollographql.cache.normalized.api.DefaultRecordMerger -import com.apollographql.cache.normalized.api.EmptyMetadataGenerator +import com.apollographql.cache.normalized.api.ExpirationCacheResolver +import com.apollographql.cache.normalized.api.GlobalMaxAgeProvider +import com.apollographql.cache.normalized.api.MaxAge import com.apollographql.cache.normalized.api.MemoryCacheFactory import com.apollographql.cache.normalized.api.NormalizedCacheFactory -import com.apollographql.cache.normalized.api.ReceiveDateCacheResolver +import com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider import com.apollographql.cache.normalized.api.TypePolicyCacheKeyGenerator import com.apollographql.cache.normalized.api.normalize import com.apollographql.cache.normalized.apolloStore -import com.apollographql.cache.normalized.cacheHeaders import com.apollographql.cache.normalized.fetchPolicy +import com.apollographql.cache.normalized.maxStale import com.apollographql.cache.normalized.normalizedCache import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory -import com.apollographql.cache.normalized.storeReceiveDate +import sqlite.GetCompanyQuery +import sqlite.GetUserAdminQuery +import sqlite.GetUserEmailQuery +import sqlite.GetUserNameQuery import sqlite.GetUserQuery import kotlin.test.Test import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds class ClientSideExpirationTest { @Test - fun memoryCache() { - test(MemoryCacheFactory()) + fun globalMaxAgeMemoryCache() { + globalMaxAge(MemoryCacheFactory()) } @Test - fun sqlCache() { - test(SqlNormalizedCacheFactory()) + fun globalMaxAgeSqlCache() { + globalMaxAge(SqlNormalizedCacheFactory()) } @Test - fun chainedCache() { - test(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + fun globalMaxAgeChainedCache() { + globalMaxAge(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) } - private fun test(normalizedCacheFactory: NormalizedCacheFactory) = runTest { + @Test + fun schemaCoordinatesMaxAgeMemoryCache() { + schemaCoordinatesMaxAge(MemoryCacheFactory()) + } + + @Test + fun schemaCoordinatesMaxAgeSqlCache() { + schemaCoordinatesMaxAge(SqlNormalizedCacheFactory()) + } + + @Test + fun schemaCoordinatesMaxAgeChainedCache() { + schemaCoordinatesMaxAge(MemoryCacheFactory().chain(SqlNormalizedCacheFactory())) + } + + + private fun globalMaxAge(normalizedCacheFactory: NormalizedCacheFactory) = runTest { val maxAge = 10 val client = ApolloClient.Builder() .normalizedCache( normalizedCacheFactory = normalizedCacheFactory, - cacheKeyGenerator = TypePolicyCacheKeyGenerator, - cacheResolver = ReceiveDateCacheResolver(maxAge), - recordMerger = DefaultRecordMerger, - metadataGenerator = EmptyMetadataGenerator, + cacheResolver = ExpirationCacheResolver(GlobalMaxAgeProvider(maxAge.seconds)), ) - .storeReceiveDate(true) .serverUrl("unused") .build() + client.apolloStore.clearAll() + val query = GetUserQuery() - val data = GetUserQuery.Data(GetUserQuery.User("John", "john@doe.com")) + val data = GetUserQuery.Data(GetUserQuery.User("John", "john@doe.com", true)) val records = query.normalize(data, CustomScalarAdapters.Empty, TypePolicyCacheKeyGenerator).values @@ -69,7 +89,7 @@ class ClientSideExpirationTest { // with max stale, should succeed val response1 = client.query(GetUserQuery()).fetchPolicy(FetchPolicy.CacheOnly) - .cacheHeaders(CacheHeaders.Builder().addHeader(ApolloCacheHeaders.MAX_STALE, "10").build()) + .maxStale(10.seconds) .execute() assertTrue(response1.data?.user?.name == "John") @@ -82,7 +102,87 @@ class ClientSideExpirationTest { assertTrue(response2.data?.user?.name == "John") } - private fun cacheHeaders(receivedDate: Long): CacheHeaders { - return CacheHeaders.Builder().addHeader(ApolloCacheHeaders.RECEIVED_DATE, receivedDate.toString()).build() + private fun schemaCoordinatesMaxAge(normalizedCacheFactory: NormalizedCacheFactory) = runTest { + val maxAgeProvider = SchemaCoordinatesMaxAgeProvider( + mapOf( + "User" to MaxAge.Duration(10.seconds), + "User.name" to MaxAge.Duration(5.seconds), + "User.email" to MaxAge.Duration(2.seconds), + ), + defaultMaxAge = 20.seconds, + ) + + val client = ApolloClient.Builder() + .normalizedCache( + normalizedCacheFactory = normalizedCacheFactory, + cacheResolver = ExpirationCacheResolver(maxAgeProvider), + ) + .serverUrl("unused") + .build() + client.apolloStore.clearAll() + + // Store records 25 seconds ago, more than default max age: should cache miss + mergeCompanyQueryResults(client, 25) + var e = client.query(GetCompanyQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 15 seconds ago, less than default max age: should not cache miss + mergeCompanyQueryResults(client, 15) + // Company fields are not configured so the default max age should be used + val companyResponse = client.query(GetCompanyQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(companyResponse.data?.company?.id == "42") + + + // Store records 15 seconds ago, more than max age for User: should cache miss + mergeUserQueryResults(client, 15) + e = client.query(GetUserAdminQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 5 seconds ago, less than max age for User: should not cache miss + mergeUserQueryResults(client, 5) + val userAdminResponse = client.query(GetUserAdminQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userAdminResponse.data?.user?.admin == true) + + + // Store records 10 seconds ago, more than max age for User.name: should cache miss + mergeUserQueryResults(client, 10) + e = client.query(GetUserNameQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 2 seconds ago, less than max age for User.name: should not cache miss + mergeUserQueryResults(client, 2) + val userNameResponse = client.query(GetUserNameQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userNameResponse.data?.user?.name == "John") + + + // Store records 5 seconds ago, more than max age for User.email: should cache miss + mergeUserQueryResults(client, 5) + e = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException + assertTrue(e.stale) + + // Store records 1 second ago, less than max age for User.email: should not cache miss + mergeUserQueryResults(client, 1) + val userEmailResponse = client.query(GetUserEmailQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute() + assertTrue(userEmailResponse.data?.user?.email == "john@doe.com") } + + private fun mergeCompanyQueryResults(client: ApolloClient, secondsAgo: Int) { + val data = GetCompanyQuery.Data(GetCompanyQuery.Company("42")) + val records = GetCompanyQuery().normalize(data, CustomScalarAdapters.Empty, TypePolicyCacheKeyGenerator).values + client.apolloStore.accessCache { + it.merge(records, cacheHeaders(currentTimeMillis() / 1000 - secondsAgo), DefaultRecordMerger) + } + } + + private fun mergeUserQueryResults(client: ApolloClient, secondsAgo: Int) { + val data = GetUserQuery.Data(GetUserQuery.User("John", "john@doe.com", true)) + val records = GetUserQuery().normalize(data, CustomScalarAdapters.Empty, TypePolicyCacheKeyGenerator).values + client.apolloStore.accessCache { + it.merge(records, cacheHeaders(currentTimeMillis() / 1000 - secondsAgo), DefaultRecordMerger) + } + } +} + +fun cacheHeaders(receivedDate: Long): CacheHeaders { + return CacheHeaders.Builder().addHeader(ApolloCacheHeaders.RECEIVED_DATE, receivedDate.toString()).build() } diff --git a/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt new file mode 100644 index 0000000..9ccff8e --- /dev/null +++ b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt @@ -0,0 +1,122 @@ +package test + +import com.apollographql.apollo.api.CompiledField +import com.apollographql.cache.normalized.api.MaxAge +import com.apollographql.cache.normalized.api.MaxAgeContext +import com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider +import sqlite.GetBookTitleQuery +import sqlite.GetCachedBookCachedTitleQuery +import sqlite.GetCachedBookTitleQuery +import sqlite.GetNodesQuery +import sqlite.GetProductQuery +import sqlite.GetProductsQuery +import sqlite.GetReaderBookTitleQuery +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +class SchemaCoordinatesMaxAgeProviderTest { + @Test + fun apolloServerExample() { + // Taken from https://www.apollographql.com/docs/apollo-server/performance/caching/#example-maxage-calculations + val provider = SchemaCoordinatesMaxAgeProvider( + coordinatesToMaxAges = mapOf( + "Query.cachedBook" to MaxAge.Duration(60.seconds), + "Query.reader" to MaxAge.Duration(40.seconds), + "Book.cachedTitle" to MaxAge.Duration(30.seconds), + "Reader.book" to MaxAge.Inherit, + ), + defaultMaxAge = 0.seconds, + ) + + var maxAge = provider.getMaxAge( + MaxAgeContext(GetBookTitleQuery().rootField().path("book", "cachedTitle")) + ) + assertEquals(0.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(GetCachedBookTitleQuery().rootField().path("cachedBook", "title")) + ) + assertEquals(60.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(GetCachedBookCachedTitleQuery().rootField().path("cachedBook", "cachedTitle")) + ) + assertEquals(30.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(GetReaderBookTitleQuery().rootField().path("reader", "book", "title")) + ) + assertEquals(40.seconds, maxAge) + } + + @Test + fun interfaceAndObject() { + val provider = SchemaCoordinatesMaxAgeProvider( + coordinatesToMaxAges = mapOf( + "Product" to MaxAge.Duration(60.seconds), + "Node" to MaxAge.Duration(30.seconds), + ), + defaultMaxAge = 0.seconds, + ) + var maxAge = provider.getMaxAge( + MaxAgeContext(GetProductQuery().rootField().path("product", "id")) + ) + // Product implements Node but it's irrelevant, the type of Query.product is Product so that's what's used + assertEquals(60.seconds, maxAge) + + maxAge = provider.getMaxAge( + MaxAgeContext(GetNodesQuery().rootField().path("node", "id")) + ) + assertEquals(30.seconds, maxAge) + } + + @Test + fun fallbackValue() { + val provider1 = SchemaCoordinatesMaxAgeProvider( + coordinatesToMaxAges = mapOf(), + defaultMaxAge = 12.seconds, + ) + var maxAge = provider1.getMaxAge( + MaxAgeContext(GetProductsQuery().rootField().path("currentUserId")) + ) + // root fields have the default maxAge + assertEquals(12.seconds, maxAge) + + maxAge = provider1.getMaxAge( + MaxAgeContext(GetProductsQuery().rootField().path("products")) + ) + // root fields have the default maxAge + assertEquals(12.seconds, maxAge) + + maxAge = provider1.getMaxAge( + MaxAgeContext(GetProductsQuery().rootField().path("products", "id")) + ) + // non root fields that return a leaf type inherit the maxAge of their parent field + assertEquals(12.seconds, maxAge) + + val provider2 = SchemaCoordinatesMaxAgeProvider( + coordinatesToMaxAges = mapOf( + "Product" to MaxAge.Duration(60.seconds), + ), + defaultMaxAge = 12.seconds, + ) + // non root fields that return a leaf type inherit the maxAge of their parent field + maxAge = provider2.getMaxAge( + MaxAgeContext(GetProductsQuery().rootField().path("products", "id")) + ) + assertEquals(60.seconds, maxAge) + + // fields that return a composite type have the default maxAge + maxAge = provider2.getMaxAge( + MaxAgeContext(GetProductsQuery().rootField().path("products", "colors")) + ) + assertEquals(12.seconds, maxAge) + } +} + +private fun CompiledField.field(name: String): CompiledField = + selections.firstOrNull { (it as CompiledField).name == name } as CompiledField + +private fun CompiledField.path(vararg path: String): List = + path.fold(listOf(this)) { acc, name -> acc + acc.last().field(name) } diff --git a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt index 39bee25..9872273 100644 --- a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt +++ b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt @@ -5,11 +5,14 @@ import com.apollographql.apollo.api.ApolloResponse import com.apollographql.apollo.exception.CacheMissException import com.apollographql.apollo.testing.internal.runTest import com.apollographql.cache.normalized.FetchPolicy -import com.apollographql.cache.normalized.api.ExpireDateCacheResolver +import com.apollographql.cache.normalized.api.ExpirationCacheResolver +import com.apollographql.cache.normalized.api.MaxAgeContext +import com.apollographql.cache.normalized.api.MaxAgeProvider import com.apollographql.cache.normalized.api.MemoryCacheFactory import com.apollographql.cache.normalized.api.NormalizedCacheFactory -import com.apollographql.cache.normalized.api.TypePolicyCacheKeyGenerator +import com.apollographql.cache.normalized.apolloStore import com.apollographql.cache.normalized.fetchPolicy +import com.apollographql.cache.normalized.maxStale import com.apollographql.cache.normalized.normalizedCache import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory import com.apollographql.cache.normalized.storeExpirationDate @@ -18,6 +21,7 @@ import com.apollographql.mockserver.MockServer import sqlite.GetUserQuery import kotlin.test.Test import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds class ServerSideExpirationTest { @Test @@ -41,25 +45,32 @@ class ServerSideExpirationTest { val client = ApolloClient.Builder() .normalizedCache( normalizedCacheFactory = normalizedCacheFactory, - cacheKeyGenerator = TypePolicyCacheKeyGenerator, - cacheResolver = ExpireDateCacheResolver() + cacheResolver = ExpirationCacheResolver( + // Can be any value since we don't store the receive date + object : MaxAgeProvider { + override fun getMaxAge(maxAgeContext: MaxAgeContext) = 0.seconds + } + ) ) .storeExpirationDate(true) .serverUrl(mockServer.url()) .build() + client.apolloStore.clearAll() + val query = GetUserQuery() val data = """ { "data": { "user": { "name": "John", - "email": "john@doe.com" + "email": "john@doe.com", + "admin": true } } } """.trimIndent() - val response: ApolloResponse + var response: ApolloResponse // store data with an expiration date in the future mockServer.enqueue( @@ -84,5 +95,12 @@ class ServerSideExpirationTest { // read from cache -> it should fail val e = client.query(GetUserQuery()).fetchPolicy(FetchPolicy.CacheOnly).execute().exception as CacheMissException assertTrue(e.stale) + + // read from cache with a max stale -> no cache miss + response = client.query(GetUserQuery()) + .fetchPolicy(FetchPolicy.CacheOnly) + .maxStale(1.seconds) + .execute() + assertTrue(response.data?.user?.name == "John") } }