Skip to content
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

Result Context, watchMutation, CacheRereadPolicy #795

Merged
merged 11 commits into from
Jan 24, 2021
62 changes: 56 additions & 6 deletions packages/graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ As of `v4`, it is built on foundational libraries from the [gql-dart project], i
- [GraphQL Upload](#graphql-upload)
- [Subscriptions](#subscriptions)
- [`client.watchQuery` and `ObservableQuery`](#clientwatchquery-and-observablequery)
- [`client.watchMutation`](#clientwatchmutation)
- [Direct Cache Access API](#direct-cache-access-api)
- [`Request`, `readQuery`, and `writeQuery`](#request-readquery-and-writequery)
- [`FragmentRequest`, `readFragment`, and `writeFragment`](#fragmentrequest-readfragment-and-writefragment)
- [Other Cache Considerations](#other-cache-considerations)
- [Write strictness and `partialDataPolicy`](#write-strictness-and-partialdatapolicy)
- [Possible cache write exceptions](#possible-cache-write-exceptions)
- [Policies](#policies)
- [Rebroadcasting](#rebroadcasting)
- [Exceptions](#exceptions)
- [Links](#links)
- [Composing Links](#composing-links)
Expand Down Expand Up @@ -310,7 +312,7 @@ subscription.listen(reactToAddedReview)
### `client.watchQuery` and `ObservableQuery`

[`client.watchQuery`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLClient/watchQuery.html)
can be used to execute both queries and mutations, then reactively listen to changes to the underlying data in the cache. It is used in the `Query` and `Mutation` widgets of `graphql_flutter`:
can be used to execute both queries and mutations, then reactively listen to changes to the underlying data in the cache.

```dart
final observableQuery = client.watchQuery(
Expand Down Expand Up @@ -351,7 +353,15 @@ observableQuery.close();

`ObservableQuery` is a bit of a kitchen sink for reactive operation logic – consider looking at the [API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/ObservableQuery-class.html) if you'd like to develop a deeper understanding.

> **NB**: `watchQuery` and `ObservableQuery` currently don't have a nice APIs for `update` `onCompleted` and `onError` callbacks,
### `client.watchMutation`

The default `CacheRereadPolicy` of `client.watchQuery` merges optimistic data from the cache into the result on every related cache change. This is great for queries, but [an undesirable default for mutations](https://github.com/zino-app/graphql-flutter/issues/774), as their results should not change due to subsequent mutations.

While eventually [we would like to decouple mutation and query logic](https://github.com/zino-app/graphql-flutter/issues/798), for now we have `client.watchMutation` (used in the `Mutation` widget of `graphql_flutter`) which has the default policy `CacheRereadPolicy.ignoreAll`. **Otherwise, its behavior is exactly the same.** It still takes `WatchQueryOptions` and returns `ObservableQuery`, and both methods can take either mutation or query documents. The `watchMutation` method should be thought of as a stop-gap.

See [Rebroadcasting](#rebroadcasting) for more details.

> **NB**: `watchQuery`, `watchMutation`, and `ObservableQuery` currently don't have a nice APIs for `update` `onCompleted` and `onError` callbacks,
> but you can have a look at how `graphql_flutter` registers them through
> [`onData`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/ObservableQuery/onData.html) in
> [`Mutation.runMutation`](https://pub.dev/documentation/graphql_flutter/4.0.0-alpha.7/graphql_flutter/MutationState/runMutation.html).
Expand Down Expand Up @@ -465,13 +475,38 @@ At link execution time, one of the following exceptions can be thrown:

## Policies

Policies are used to configure execution and error behavior for a given request.
The client's default policies can also be set for each method via the `defaultPolicies` option.
Policies are used to configure various aspects of a request process, and can be set on any `*Options` object:
```dart
// override policies for a single query
client.query(QueryOptions(
// return result from network and save to cache.
fetchPolicy: FetchPolicy.networkOnly,
// ignore all GraphQL errors.
errorPolicy: ErrorPolicy.ignore,
// ignore cache data.
cacheRereadPolicy: CacheRereadPolicy.ignore,
// ...
));
```
Defaults can also be overridden via `defaultPolices` on the client itself:
```dart
GraphQLClient(
defaultPolicies: DefaultPolicies(
// make watched mutations behave like watched queries.
watchMutation: Policies(
FetchPolicy.cacheAndNetwork,
ErrorPolicy.none,
CacheRereadPolicy.mergeOptimistic,
),
),
// ...
)
```

**[`FetchPolicy`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/FetchPolicy-class.html):** determines where the client may return a result from.
**[`FetchPolicy`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/FetchPolicy-class.html):** determines where the client may return a result from, and whether that result will be saved to the cache.
Possible options:

- cacheFirst (default): return result from cache. Only fetch from network if cached result is not available.
- cacheFirst: return result from cache. Only fetch from network if cached result is not available.
- cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available.
- cacheOnly: return result from cache if available, fail otherwise.
- noCache: return result from network, fail if network call doesn't succeed, don't save to cache.
Expand All @@ -486,6 +521,21 @@ Possible options:
- all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server.
It saves both data and errors into the Apollo Cache so your UI can use them.

**CacheRereadPolicy** determines whether and how cache data will be merged into the final `QueryResult.data` before it is returned.
Possible options:
* mergeOptimistic: Merge relevant optimistic data from the cache before returning.
* ignoreOptimistic: Ignore optimistic data, but still allow for non-optimistic cache rebroadcasts
**if applicable**.
* ignoreAll: Ignore all cache data besides the result, and never rebroadcast the result,
even if the underlying cache data changes.

### Rebroadcasting
Rebroadcasting behavior only applies to `watchMutation` and `watchQuery`, which both return an `ObservableQuery`.
There is no rebroadcasting option for subscriptions, because it would be indistiguishable from the previous event in the stream.

Rebroadcasting is enabled unless either `FetchPolicy.noCache` or `CacheRereadPolicy.ignoreAll` are set,
and whether it considers optimistic results is controlled by the specific `CacheRereadPolicy`.

## Exceptions

If there were problems encountered during a query or mutation, the `QueryResult` will have an `OperationException` in the `exception` field:
Expand Down
9 changes: 8 additions & 1 deletion packages/graphql/lib/src/core/_base_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ abstract class BaseOptions extends MutableDataClass {
Context context,
FetchPolicy fetchPolicy,
ErrorPolicy errorPolicy,
CacheRereadPolicy cacheRereadPolicy,
this.optimisticResult,
}) : policies = Policies(fetch: fetchPolicy, error: errorPolicy),
}) : policies = Policies(
fetch: fetchPolicy,
error: errorPolicy,
cacheReread: cacheRereadPolicy,
),
context = context ?? Context();

/// Document containing at least one [OperationDefinitionNode]
Expand All @@ -43,6 +48,8 @@ abstract class BaseOptions extends MutableDataClass {

ErrorPolicy get errorPolicy => policies.error;

CacheRereadPolicy get cacheRereadPolicy => policies.cacheReread;

/// Context to be passed to link execution chain.
Context context;

Expand Down
31 changes: 17 additions & 14 deletions packages/graphql/lib/src/core/_query_write_handling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ typedef _IntPartialDataHandler = MismatchedDataStructureException Function(

extension InternalQueryWriteHandling on QueryManager {
/// Merges exceptions into `queryResult` and
/// returns `true` if a reread should be attempted
/// returns `true` on success.
///
/// This is named `*OrSetExceptionOnQueryResult` because it is very imperative,
/// and edits the [queryResult] inplace.
Expand Down Expand Up @@ -49,30 +49,33 @@ extension InternalQueryWriteHandling on QueryManager {
/// Part of [InternalQueryWriteHandling], and not exposed outside the
/// library.
///
/// Returns `true` if a reread should be attempted to incorporate potential optimistic data.
///
/// If we have no data, we skip caching, thus taking [ErrorPolicy.none]
/// into account
/// into account.
///
/// networked wrapper for [_writeQueryOrSetExceptionOnQueryResult]
/// NOTE: mapFetchResultToQueryResult must be called beforehand
bool attemptCacheWriteFromResponse(
FetchPolicy fetchPolicy,
Policies policies,
Request request,
Response response,
QueryResult queryResult,
) =>
(fetchPolicy == FetchPolicy.noCache || queryResult.data == null)
(policies.fetch == FetchPolicy.noCache || queryResult.data == null)
? false
: _writeQueryOrSetExceptionOnQueryResult(
request,
response.data,
queryResult,
writeQuery: (req, data) => cache.writeQuery(req, data: data),
onPartial: (failure) => UnexpectedResponseStructureException(
failure,
request: request,
parsedResponse: response,
),
);
request,
response.data,
queryResult,
writeQuery: (req, data) => cache.writeQuery(req, data: data),
onPartial: (failure) => UnexpectedResponseStructureException(
failure,
request: request,
parsedResponse: response,
),
) &&
policies.mergeOptimisticData;

/// Part of [InternalQueryWriteHandling], and not exposed outside the
/// library.
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/lib/src/core/mutation_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class MutationOptions extends BaseOptions {
Map<String, dynamic> variables = const {},
FetchPolicy fetchPolicy,
ErrorPolicy errorPolicy,
CacheRereadPolicy cacheRereadPolicy,
Context context,
Object optimisticResult,
this.onCompleted,
Expand All @@ -39,6 +40,7 @@ class MutationOptions extends BaseOptions {
super(
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
cacheRereadPolicy: cacheRereadPolicy,
document: document ?? documentNode,
operationName: operationName,
variables: variables,
Expand Down
5 changes: 4 additions & 1 deletion packages/graphql/lib/src/core/observable_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,13 @@ class ObservableQuery {
}

/// Whether it is safe to rebroadcast results due to cache
/// changes based on [lifecycle].
/// changes based on policies and [lifecycle].
///
/// Called internally by the [QueryManager]
bool get isRebroadcastSafe {
if (!options.policies.allowsRebroadcasting) {
return false;
}
switch (lifecycle) {
case QueryLifecycle.pending:
case QueryLifecycle.completed:
Expand Down
Loading