diff --git a/Writerside/doc.tree b/Writerside/doc.tree index 088a405c..58769efe 100644 --- a/Writerside/doc.tree +++ b/Writerside/doc.tree @@ -19,6 +19,7 @@ + diff --git a/Writerside/topics/compiler-plugin.md b/Writerside/topics/compiler-plugin.md index 0184c588..9e1c0c3d 100644 --- a/Writerside/topics/compiler-plugin.md +++ b/Writerside/topics/compiler-plugin.md @@ -59,8 +59,9 @@ object cache { } ``` +This map is passed to the `TypePolicyCacheKeyGenerator` when calling [the `cache()` extension](#cache-extension-function). -Pass this map to the `TypePolicyCacheKeyGenerator` when configuring the cache: +If you need more control over the configuration, use the `normalizedCache()` extension and pass this map to the `TypePolicyCacheKeyGenerator`: ```kotlin val apolloClient = ApolloClient.Builder() @@ -107,7 +108,7 @@ can use the value of `__typename` as the type of the returned object, and from t From that it can return `User:42` as the cache key for that record. -> If your schema has ids that are unique across the service, you can pass `CacheKey.Scope.SERVICE` to the `TypePolicyCacheKeyGenerator` constructor to save space in the cache. +> If your schema has ids that are unique across the service, you can pass `CacheKey.Scope.SERVICE` to the [`cache()`](#cache-extension-function) extension or `TypePolicyCacheKeyGenerator` constructor to save space in the cache. > > In that example the cache key would be `42` instead of `User:42`. @@ -234,8 +235,21 @@ thus saving a network request if the record is already in the cache. #### Unions and interfaces {id="field-policy-unions-and-interfaces"} If a field returns a union or interface it is not possible to know which concrete type will be returned at runtime, and thus prefixing -the cache key with the correct type name is not possible. Avoiding a network call is not possible here. +the cache key with the correct type name is not possible. A network call can't be avoided here. -However, if your schema has ids that are unique across the service, you can pass `CacheKey.Scope.SERVICE` to the `FieldPolicyCacheResolver` constructor to skip the type name in the cache key. -Network call avoidance will still work in that case. +However, if your schema has ids that are unique across the service, you can pass `CacheKey.Scope.SERVICE` to the [`cache()`](#cache-extension-function) extension or `FieldPolicyCacheResolver` constructor to skip the type name in the cache key. +Network call avoidance will work in that case. + +## `cache()` extension function + +An `ApolloClient.Builder.cache()` extension function is generated by the compiler plugin, which configures the `CacheKeyGenerator`, `MetadataGenerator`, `CacheResolver`, and `RecordMerger` based +on the type policies, connection types, and [max ages](cache-control.md) configured in the schema: + +```kotlin +val apolloClient = ApolloClient.Builder() + // ... + .cache(cacheFactory = /*...*/) + .build() +``` +Optionally pass a `defaultMaxAge` (infinity by default) and `keyScope` (`CacheKey.Scope.TYPE` by default). diff --git a/Writerside/topics/migration-guide.md b/Writerside/topics/migration-guide.md index 697267bb..8ba21089 100644 --- a/Writerside/topics/migration-guide.md +++ b/Writerside/topics/migration-guide.md @@ -106,7 +106,7 @@ Previously, write methods had 2 flavors: - a `suspend` one that accepts a `publish` parameter to control whether changes should be published to watchers - a non-suspend one (e.g. `writeOperationSync`) that doesn't publish changes -Now only the non-suspend ones exist and don't publish. Manually call `publish()` to notify watchers of changes. +Now only the suspend ones exist and don't publish. Manually call `publish()` to notify watchers of changes. ```kotlin // Replace @@ -163,6 +163,12 @@ val apolloClient = ApolloClient.Builder() ### Other changes +- All operations are now `suspend`.
+Note that they may **suspend** or **block** the thread depending on the underlying cache +implementation. For example, the SQL cache implementation on Android will **block** the thread while accessing the disk. As such, +these operations **must not** run on the main thread. You can enclose them in a `withContext` block with a `Dispatchers.IO` context to ensure +that they run on a background thread. + - `readFragment()` now returns a `ReadResult` (it previously returned a ``). This allows for surfacing metadata associated to the returned data, e.g. staleness. - Records are now rooted per operation type (`QUERY_ROOT`, `MUTATION_ROOT`, `SUBSCRIPTION_ROOT`), when previously these were all at the same level, which could cause conflicts. @@ -190,6 +196,9 @@ interface CacheResolver { `resolveField` can also now return a `ResolvedValue` when metadata should be returned with the resolved value (e.g. staleness). +If you wish to use the [Cache Control](cache-control.md) feature, a [`CacheControlCacheResolver`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized.api/-cache-control-cache-resolver/index.html) should be used. +You can call the generated `cache()` extension on `ApolloClient.Builder` which will use it by default if max ages are configured in the schema. + ### `TypePolicyCacheKeyGenerator` You can now pass the type policies to the `TypePolicyCacheKeyGenerator` constructor, and it is recommended to do so. @@ -248,3 +257,16 @@ For consistency, the `CacheKey` type is now used instead of `String` in more API - `ApolloCacheHeaders.EVICT_AFTER_READ` is removed. Manually call `ApolloStore.remove()` when needed instead. - `NormalizedCache.remove(pattern: String)` is removed. Please open an issue if you need this feature back. + +## Other changes {id="other-changes-2"} + +An `ApolloClient.Builder.cache()` extension function is generated by the compiler plugin, which configures the `CacheKeyGenerator`, `MetadataGenerator`, `CacheResolver`, and `RecordMerger` based +on the type policies, connection types, and max ages configured in the schema: + +```kotlin +val apolloClient = ApolloClient.Builder() + // ... + .cache(cacheFactory = /*...*/) + .build() +``` +Optionally pass a `defaultMaxAge` (infinity by default) and `keyScope` (`TYPE` by default). diff --git a/Writerside/topics/partial-cache-reads.md b/Writerside/topics/partial-cache-reads.md new file mode 100644 index 00000000..0ab61c96 --- /dev/null +++ b/Writerside/topics/partial-cache-reads.md @@ -0,0 +1,54 @@ +# Partial cache reads + +The cache supports partial cache reads, in a similar way to how GraphQL supports partial responses. +This means that if some fields are missing from the cache, the cache will return the available data along with any errors for the missing fields. + +## With `ApolloStore` + +The [`ApolloStore.readOperation()`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized/-apollo-store/read-operation.html) +API returns an `ApolloResponse` that can contain partial `data` and non-empty `errors` for any missing (or stale) fields in the cache. + +> `ApolloResponse.cacheInfo.isCacheHit` will be false when any field is missing. +> `ApolloResponse.cacheInfo.isStale` will be true when any field is stale. + +If a cache miss exception is preferred, you can use the `ApolloResponse.errorsAsException()` extension function that returns +a response with an exception if there are any errors. + +## With `ApolloClient` + +When executing operations, the built-in fetch policies ([`FetchPolicy.CacheFirst`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized/-fetch-policy/-cache-first/index.html?query=CacheFirst), +[`FetchPolicy.CacheOnly`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized/-fetch-policy/-cache-only/index.html), +etc.) will treat cache misses as exceptions (partial results are disabled). + +To benefit from partial cache reads, implement your own fetch policy interceptor as shown in this example: + +```kotlin +object PartialCacheOnlyInterceptor : ApolloInterceptor { + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain + ): Flow> { + return chain.proceed( + request = request + .newBuilder() + // Controls where to read the data from (cache or network) + .fetchFromCache(true) + .build() + ) + } +} + +// ... + +val apolloClient = ApolloClient.Builder() + /*...*/ + .serverUrl("https://example.com/graphql") + .fetchPolicyInterceptor(PartialCacheOnlyInterceptor) + .build() +``` + +## Error stored in the cache + +Errors from the server are stored in the cache, and will be returned when reading it. + +By default, errors don't replace existing data in the cache. You can change this behavior with [`errorsReplaceCachedValues(true)`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized/errors-replace-cached-values.html?query=fun%20%3CT%3E%20MutableExecutionOptions%3CT%3E.errorsReplaceCachedValues(errorsReplaceCachedValues:%20Boolean):%20T).