diff --git a/Writerside/doc.tree b/Writerside/doc.tree
index f8bf4845..d659899d 100644
--- a/Writerside/doc.tree
+++ b/Writerside/doc.tree
@@ -10,4 +10,5 @@
+
diff --git a/Writerside/topics/expiration.md b/Writerside/topics/expiration.md
new file mode 100644
index 00000000..1b1512bf
--- /dev/null
+++ b/Writerside/topics/expiration.md
@@ -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()
+```
diff --git a/Writerside/topics/pagination.md b/Writerside/topics/pagination.md
index 7a213ce4..5f12d5c5 100644
--- a/Writerside/topics/pagination.md
+++ b/Writerside/topics/pagination.md
@@ -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:
diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.api b/normalized-cache-incubating/api/normalized-cache-incubating.api
index 0a3c5116..7b703e69 100644
--- a/normalized-cache-incubating/api/normalized-cache-incubating.api
+++ b/normalized-cache-incubating/api/normalized-cache-incubating.api
@@ -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 ()V
public fun (Lcom/apollographql/cache/normalized/api/MaxAgeProvider;)V
public fun resolveField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Ljava/lang/Object;
}
diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api
index 503b9bc3..727db72b 100644
--- a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api
+++ b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api
@@ -177,6 +177,7 @@ final class com.apollographql.cache.normalized.api/EmbeddedFieldsContext { // co
final fun (): com.apollographql.apollo.api/CompiledNamedType // com.apollographql.cache.normalized.api/EmbeddedFieldsContext.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 () // com.apollographql.cache.normalized.api/ExpirationCacheResolver.|(){}[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]
}
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 a7c7a6db..07e0e851 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
@@ -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.
@@ -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) {
diff --git a/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt
index 01a664cc..a6129c70 100644
--- a/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt
+++ b/tests/expiration/src/commonTest/kotlin/SchemaCoordinatesMaxAgeProviderTest.kt
@@ -103,7 +103,7 @@ class SchemaCoordinatesMaxAgeProviderTest {
@Test
fun fallbackValue() {
val provider1 = SchemaCoordinatesMaxAgeProvider(
- maxAges = mapOf(),
+ maxAges = mapOf(),
defaultMaxAge = 12.seconds,
)
var maxAge = provider1.getMaxAge(
diff --git a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt
index cd5f1a13..eece2227 100644
--- a/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt
+++ b/tests/expiration/src/commonTest/kotlin/ServerSideExpirationTest.kt
@@ -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
@@ -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())