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

Unclear error when attempting to mutate with FetchPolicy.cacheOnly #792

Open
perilousGourd opened this issue Jan 14, 2021 · 1 comment
Open

Comments

@perilousGourd
Copy link

perilousGourd commented Jan 14, 2021

Issue
If a mutation is performed with the fetch policy cacheOnly before it is performed with cacheFirst or cacheAndNetwork, it will fail with a CacheMissException.

To reproduce

Run this minimal working example, tap the mutate button and check the queryResult printed to console:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() {
  runApp(
    Directionality(
      textDirection: TextDirection.ltr,
      child: Container(
        color: Colors.blue,
        child: OfflineExperimenting4(),
      ),
    ),
  );
}

class OfflineExperimenting4 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Future<ValueNotifier<GraphQLClient>> clientNotifier =
        GraphQLConfiguration().client().then((client) {
      return ValueNotifier<GraphQLClient>(client);
    });

    return FutureBuilder(
        future: clientNotifier,
        builder: (
          context,
          AsyncSnapshot<ValueNotifier<GraphQLClient>> snapshot,
        ) {
          if (snapshot.hasData) {
            return GraphQLProvider(
              client: snapshot.data,
              child: CacheProvider(
                child: MutateWidget(),
              ),
            );
          } else {
            return Container(
              alignment: Alignment.center,
              child: Text('loading!'),
            );
          }
        });
  }
}

class MutateWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: GestureDetector(
        onTap: () async {
          GraphQLClient client = GraphQLProvider.of(context).value;

          QueryResult queryResultMutation = await client.mutate(
            MutationOptions(
              document: gql('''
            mutation createTodo(\$content: String!) {
              createTodo(input: {title: \$content, completed: true}) {
                title,
              }
            }
            '''),
              variables: {
                "content": "I am content.",
              },
                // fetchPolicy: FetchPolicy.cacheFirst
              fetchPolicy: FetchPolicy.cacheOnly,
            ),
          );

          print('queryResult = ' + queryResultMutation.toString());
        },
        child: Container(
          child: Text('mutate'),
          color: Colors.white.withOpacity(0.2),
          padding: EdgeInsets.all(20),
        ),
      ),
    );
  }
}

class GraphQLConfiguration {
  HttpLink link = HttpLink("https://graphqlzero.almansi.me/api");

  Future<GraphQLClient> client() async {
    await initHiveForFlutter();

    var testBox = await HiveStore.openBox('testBox');
    GraphQLClient client = GraphQLClient(
      link: link,
      cache: GraphQLCache(
        store: HiveStore(testBox),
      ),
    );

    return client;
  }
}

Note that this example uses graphql_flutter: ^4.0.0-beta.1, as a comment on issue #597 recommended, but I experienced this also with 3.1.0.
Might be related to #779 also?

Expected behaviour
I was expecting the mutation to be cached without an exception, and with potential to be synced with network database in future.

Devices
Android and web.

Further details and context
In my example, if the hardcoded mutation in MutateWidget is first performed with the fetch policy cacheOnly, it will fail with a CacheMissException.
If it is first performed with cacheFirst or cacheAndNetwork, changing the fetch policy to cacheOnly, hot reloading and submitting the exact same mutation succeeds. If the mutation is not the exact same, e.g. if, in this example, "content": "I am content." is changed by one character to "content": "I am content!", a CacheMissException occurs.

This seems problematic because saving mutations to cache via cacheOnly when the mutation variables are dependent on user input will therefore rarely (only in the case of duplicate data entries) succeed.

At the moment, I care about this because I want user input to be saved to cache as it is entered, so it persists if app crashes or is closed, and only synced with an online database when the user chooses, with the goal of reducing unwanted/superfluous network requests.

Noob questions (feel free to ignore):

I'm new to GraphQL, databases, and even app development, so if I seem to be missing a concept, I probably am (and would be grateful to be told what it is)!

I think I've gathered that CacheMissExceptions occur when a query requests information that is not compatible 'structurally' with any of the information that exists in the cache, e.g. when querying the cache prior to ever querying the network database, or requesting e.g. 'title' and 'user' from cache when only 'title' has ever been requested from network.

It makes sense to me why these queries return exceptions when cacheOnly, as sufficient information to complete them is not available in cache. I don't understand why attempting to perform a creation mutation with cacheOnly results in this exception, though, as all?* information about the object to be added is being provided to the cache by the app.
*Is it something to do with having no access to the GraphQL schema and thereby no ability to determine if the shape of the mutation is valid, all required fields are provided etc?

@perilousGourd perilousGourd added the v4 Issue with a v4 library label Jan 14, 2021
@micimize
Copy link
Collaborator

So, fundamentally what you're looking for here is an Offline Mutation Queue (#201). While the dream of offline-first design, we don't currently supply the ability to maintain that kind of long-term optimistic client state / eventual synchronization.

The error here is not very informative, which is itself an issue, but basically a mutation with FetchPolicy.cacheOnly only attempts to retrieved the cached result of the latest call to that mutation. This fails, and returns an exception because there's no remaining execution path that can return data:
https://github.com/zino-app/graphql-flutter/blob/8e4e7d95808b75635dc4d0ad1fd78f653e283058/packages/graphql/lib/src/core/query_manager.dart#L262-L273

In your example, there is no way for us to know what the effect of createTodo should be on the cache
Really, more context should be added. Maybe we should disallow the option entirely, and say something like "FetchPolicy.cacheOnly is an invalid FetchPolicy for mutations. If you are trying to update cache data, you may be looking for client.writeQuery. If you are trying to get the previous mutation result, you may be looking for client.readQuery"

Feel free to come by the discord #support with questions also – prob could have saved you a fair amount of hair pulling 😅

@micimize micimize changed the title CacheMissException when mutating with cacheOnly fetch policy in 3.1.0 and 4.0.0 Unclear error when attempting to mutate with FetchPolicy.cacheOnly Jan 24, 2021
@micimize micimize removed the v4 Issue with a v4 library label Jan 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants