Skip to content

Add CacheInfo.isStale #53

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 7 commits into from
Oct 29, 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Next version (unreleased)

- Expiration support (see [the documentation](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/expiration.html) for details)
- Cache control support (see [the documentation](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/cache-control.html) for details)
- Compatibility with the IntelliJ plugin cache viewer (#42)
- For consistency, `MemoryCacheFactory` and `MemoryCache` are now in the `com.apollographql.cache.normalized.memory` package
- Remove deprecated symbols
Expand Down
2 changes: 1 addition & 1 deletion Writerside/doc.tree
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +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"/>
<toc-element topic="cache-control.md"/>
</instance-profile>
Original file line number Diff line number Diff line change
@@ -1,53 +1,57 @@
# Expiration
# Cache control

The cache control feature takes the freshness of fields into consideration when accessing the cache. This is also sometimes referred to as TTL (Time To Live) or expiration.

Freshness can be configured by the server, by the client, or both.

## 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):
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 [`CacheControlCacheResolver`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-cache-control-cache-resolver/index.html):

```kotlin
val apolloClient = ApolloClient.builder()
.serverUrl("https://example.com/graphql")
.storeExpirationDate(true)
.normalizedCache(
normalizedCacheFactory = /*...*/,
cacheResolver = ExpirationCacheResolver(),
cacheResolver = CacheControlCacheResolver(),
)
.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`.
**Expiration dates** will be stored and when a field is resolved, the cache resolver will check if the field is stale. 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):
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 [`CacheControlCacheResolver`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized.api/-cache-control-cache-resolver/index.html):

```kotlin
val apolloClient = ApolloClient.builder()
.serverUrl("https://example.com/graphql")
.storeReceivedDate(true)
.normalizedCache(
normalizedCacheFactory = /*...*/,
cacheResolver = ExpirationCacheResolver(maxAgeProvider),
cacheResolver = CacheControlCacheResolver(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`.
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 `CacheControlCacheResolver`.

### 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`:
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 `CacheControlCacheResolver`:

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

### Max age per type and field
Expand All @@ -57,17 +61,20 @@ To set a global maximum age for all fields, pass a [`GlobalMaxAgeProvider`](http
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,
)),
cacheResolver = CacheControlCacheResolver(
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
Expand Down Expand Up @@ -110,19 +117,31 @@ apollo {
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,
)),
cacheResolver = CacheControlCacheResolver(
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:
If stale fields are acceptable up to a certain value, you can set a maximum staleness duration. This duration is the maximum time that a stale field will be resolved without resulting in a cache miss. To set this duration, call [`.maxStale(Duration)`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized/max-stale.html?query=fun%20%3CT%3E%20MutableExecutionOptions%3CT%3E.maxStale(maxStale:%20Duration):%20T) either globally on your client, or per operation:

```kotlin
client.query(MyQuery())
.fetchPolicy(FetchPolicy.CacheOnly)
.maxStale(1.hours)
.execute()
val response = client.query(MyQuery())
.fetchPolicy(FetchPolicy.CacheOnly)
.maxStale(1.hours)
.execute()
```

### `isStale`

With `maxStale`, it is possible to get data from the cache even if it is stale. To know if the response contains stale fields, you can check [`CacheInfo.isStale`](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/kdoc/normalized-cache-incubating/com.apollographql.cache.normalized/-cache-info/is-stale.html):

```kotlin
if (response.cacheInfo?.isStale == true) {
// The response contains at least one stale field
}
```
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ sqldelight-jvm = { group = "app.cash.sqldelight", name = "sqlite-driver", versio
sqldelight-native = { group = "app.cash.sqldelight", name = "native-driver", version.ref = "sqldelight" }
sqldelight-runtime = { group = "app.cash.sqldelight", name = "runtime", version.ref = "sqldelight" }
truth = "com.google.truth:truth:1.1.3"
turbine = "app.cash.turbine:turbine:1.2.0"
slf4j-nop = "org.slf4j:slf4j-nop:2.0.13"
androidx-sqlite = { group = "androidx.sqlite", name = "sqlite", version.ref = "androidx-sqlite" }
androidx-sqlite-framework = { group = "androidx.sqlite", name = "sqlite-framework", version.ref = "androidx-sqlite" }
Expand Down
Loading