Skip to content

Add expiration doc #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Writerside/doc.tree
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
<toc-element toc-title="Kdoc" href="https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc"/>
<toc-element topic="welcome.md"/>
<toc-element topic="pagination.md"/>
<toc-element topic="expiration.md"/>
</instance-profile>
128 changes: 128 additions & 0 deletions Writerside/topics/expiration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Expiration

## Server-controlled

When receiving a response from the server, the [`Cache-Control` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) can be used to determine the **expiration date** of the fields in the response.

> Apollo Server can be configured to include the `Cache-Control` header in responses. See the [caching documentation](https://www.apollographql.com/docs/apollo-server/performance/caching/) for more information.

The cache can be configured to store the **expiration date** of the received fields in the corresponding records. To do so, call [`.storeExpirationDate(true)`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized/store-expiration-date.html?query=fun%20%3CT%3E%20MutableExecutionOptions%3CT%3E.storeExpirationDate(storeExpirationDate:%20Boolean):%20T), and set your client's cache resolver to [`ExpirationCacheResolver`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-expiration-cache-resolver/index.html):

```kotlin
val apolloClient = ApolloClient.builder()
.serverUrl("https://example.com/graphql")
.storeExpirationDate(true)
.normalizedCache(
normalizedCacheFactory = /*...*/,
cacheResolver = ExpirationCacheResolver(),
)
.build()
```

**Expiration dates** will be stored and when a field is resolved, the cache resolver will check if the field is expired. If so, it will throw a `CacheMissException`.

## Client-controlled

When storing fields, the cache can also store their **received date**. This date can then be compared to the current date when resolving a field to determine if its age is above its **maximum age**.

To store the **received date** of fields, call [`.storeReceivedDate(true)`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized/store-receive-date.html?query=fun%20%3CT%3E%20MutableExecutionOptions%3CT%3E.storeReceiveDate(storeReceiveDate:%20Boolean):%20T), and set your client's cache resolver to [`ExpirationCacheResolver`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-expiration-cache-resolver/index.html):

```kotlin
val apolloClient = ApolloClient.builder()
.serverUrl("https://example.com/graphql")
.storeReceivedDate(true)
.normalizedCache(
normalizedCacheFactory = /*...*/,
cacheResolver = ExpirationCacheResolver(maxAgeProvider),
)
.build()
```

> Expiration dates and received dates can be both stored to combine server-controlled and client-controlled expiration strategies.

The **maximum age** of fields can be configured either programmatically, or declaratively in the schema. This is done by passing a [`MaxAgeProvider`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-max-age-provider/index.html?query=interface%20MaxAgeProvider) to the `ExpirationCacheResolver`.

### Global max age

To set a global maximum age for all fields, pass a [`GlobalMaxAgeProvider`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-global-max-age-provider/index.html?query=class%20GlobalMaxAgeProvider(maxAge:%20Duration)%20:%20MaxAgeProvider) to the `ExpirationCacheResolver`:

```kotlin
cacheResolver = ExpirationCacheResolver(GlobalMaxAgeProvider(1.hours)),
```

### Max age per type and field

#### Programmatically

Use a [`SchemaCoordinatesMaxAgeProvider`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-schema-coordinates-max-age-provider/index.html?query=class%20SchemaCoordinatesMaxAgeProvider(maxAges:%20Map%3CString,%20MaxAge%3E,%20defaultMaxAge:%20Duration)%20:%20MaxAgeProvider) to specify a max age per type and/or field:

```kotlin
cacheResolver = ExpirationCacheResolver(SchemaCoordinatesMaxAgeProvider(
maxAges = mapOf(
"Query.cachedBook" to MaxAge.Duration(60.seconds),
"Query.reader" to MaxAge.Duration(40.seconds),
"Post" to MaxAge.Duration(4.minutes),
"Book.cachedTitle" to MaxAge.Duration(30.seconds),
"Reader.book" to MaxAge.Inherit,
),
defaultMaxAge = 1.hours,
)),
```
Note that this provider replicates the behavior of Apollo Server's [`@cacheControl` directive](https://www.apollographql.com/docs/apollo-server/performance/caching/#default-maxage) when it comes to defaults and the meaning of `Inherit`.

#### Declaratively

To declare the maximum age of types and fields in the schema, use the `@cacheControl` and `@cacheControlField` directive:

```
# First import the directives
extend schema @link(
url: "https://specs.apollo.dev/cache/v0.1",
import: ["@cacheControl", "@cacheControlField"]
)

# Then extend your types
extend type Query @cacheControl(maxAge: 60)
@cacheControlField(name: "cachedBook", maxAge: 60)
@cacheControlField(name: "reader", maxAge: 40)

extend type Post @cacheControl(maxAge: 240)

extend type Book @cacheControlField(name: "cachedTitle", maxAge: 30)

extend type Reader @cacheControlField(name: "book", inheritMaxAge: true)
```

Then configure the Cache compiler plugin in your `build.gradle.kts`:

```kotlin
apollo {
service("service") {
packageName.set(/*...*/)

plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:%latest_version%") {
argument("packageName", packageName.get())
}
}
}
```

This will generate a map in `yourpackage.cache.Cache.maxAges`, that you can pass to the `SchemaCoordinatesMaxAgeProvider`:

```kotlin
cacheResolver = ExpirationCacheResolver(SchemaCoordinatesMaxAgeProvider(
maxAges = Cache.maxAges,
defaultMaxAge = 1.hours,
)),
```

## Maximum staleness

If expired fields are acceptable up to a certain value, you can set a maximum staleness duration. This duration is the maximum time that an expired field will be resolved without resulting in a cache miss. To set this duration, call `.maxStale(Duration)` either globally on your client, or per operation:

```kotlin
client.query(MyQuery())
.fetchPolicy(FetchPolicy.CacheOnly)
.maxStale(1.hours)
.execute()
```
2 changes: 1 addition & 1 deletion Writerside/topics/pagination.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pagination with the normalized cache
# Pagination

When using the normalized cache, objects are stored in records keyed by the object's id:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ public final class com/apollographql/cache/normalized/api/EmptyMetadataGenerator
}

public final class com/apollographql/cache/normalized/api/ExpirationCacheResolver : com/apollographql/cache/normalized/api/CacheResolver {
public fun <init> ()V
public fun <init> (Lcom/apollographql/cache/normalized/api/MaxAgeProvider;)V
public fun resolveField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Ljava/lang/Object;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ final class com.apollographql.cache.normalized.api/EmbeddedFieldsContext { // co
final fun <get-parentType>(): com.apollographql.apollo.api/CompiledNamedType // com.apollographql.cache.normalized.api/EmbeddedFieldsContext.parentType.<get-parentType>|<get-parentType>(){}[0]
}
final class com.apollographql.cache.normalized.api/ExpirationCacheResolver : com.apollographql.cache.normalized.api/CacheResolver { // com.apollographql.cache.normalized.api/ExpirationCacheResolver|null[0]
constructor <init>() // com.apollographql.cache.normalized.api/ExpirationCacheResolver.<init>|<init>(){}[0]
constructor <init>(com.apollographql.cache.normalized.api/MaxAgeProvider) // com.apollographql.cache.normalized.api/ExpirationCacheResolver.<init>|<init>(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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.apollographql.cache.normalized.maxStale
import com.apollographql.cache.normalized.storeExpirationDate
import com.apollographql.cache.normalized.storeReceiveDate
import kotlin.jvm.JvmSuppressWildcards
import kotlin.time.Duration

/**
* Controls how fields are resolved from the cache.
Expand Down Expand Up @@ -147,6 +148,11 @@ object DefaultCacheResolver : CacheResolver {
class ExpirationCacheResolver(
private val maxAgeProvider: MaxAgeProvider,
) : CacheResolver {
/**
* Creates a new [ExpirationCacheResolver] with no max ages. Use this constructor if you want to consider only the expiration dates.
*/
constructor() : this(maxAgeProvider = GlobalMaxAgeProvider(Duration.INFINITE))

override fun resolveField(context: ResolverContext): Any? {
val resolvedField = FieldPolicyCacheResolver.resolveField(context)
if (context.parent is Record) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class SchemaCoordinatesMaxAgeProviderTest {
@Test
fun fallbackValue() {
val provider1 = SchemaCoordinatesMaxAgeProvider(
maxAges = mapOf<String, MaxAge>(),
maxAges = mapOf(),
defaultMaxAge = 12.seconds,
)
var maxAge = provider1.getMaxAge(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ 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.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.apolloStore
Expand Down Expand Up @@ -45,12 +43,7 @@ class ServerSideExpirationTest {
val client = ApolloClient.Builder()
.normalizedCache(
normalizedCacheFactory = normalizedCacheFactory,
cacheResolver = ExpirationCacheResolver(
// Can be any value since we don't store the receive date
object : MaxAgeProvider {
override fun getMaxAge(maxAgeContext: MaxAgeContext) = 0.seconds
}
)
cacheResolver = ExpirationCacheResolver(),
)
.storeExpirationDate(true)
.serverUrl(mockServer.url())
Expand Down