urql-graphql / urql

The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
https://urql.dev/goto/docs
MIT License
8.66k stars 454 forks source link

RFC: Offline Exchange for Graphcache #683

Closed kitten closed 4 years ago

kitten commented 4 years ago

Summary

The offlineExchange will build on Graphcache's persistence support (see #674) to bring full offline capabilities to the library.

It will be a new exchange that wraps around the cacheExchange and looks identical in its API with extended functionality.

This extended functionality supplements the persistence support by being able to queue operations when the user went offline, preventing errors from surfacing in the UI when going back online, and also reexecuting operations as necessary.

It shouldn't duplicate existing functionality, like the retryExchange, but may build on it.

Requirements

The storage adapter in Graphcache will need to be extended; this is our first draft:

export interface StorageAdapter {
  readMetadata(): Promise<string>;
  writeMetadata(json: string): any;
  readData(): Promise<SerializedEntries>;
  writeData(data: SerializedEntries): any;
  onNetworkChange(cb: () => void): void;
  isOnline(): boolean;
}
kitten commented 4 years ago

Apart from a configuration on what happens when an Optimistic Mutation is run while offline / fails because the user is offline, we haven't decided yet what the natural behaviour should be. Optimally we shouldn't even have this configuration, but the behaviour can be one of the following:

Then there's also the problem of Optimistic Mutations themselves: if a mutation contains some fields that are and some fields that aren't optimistic, then what should happen?

Are both first options valid? Should both be possible somehow? What happens in other apps when they're offline and how do we prevent inconsistent states?

morrys commented 4 years ago

@kitten, I created the offline-first library which manages the offline workflow and persistence.

I used this library to extend Apollo and Relay with @wora/apollo-offline and react-relay-offline to manage queries and mutations offline.

Let me know if you are interested in integrating it to create the offlineExchange.

wtrocki commented 4 years ago

I would like to support @morrys as an alternative to the described approach. The challenge of building offline enabled application is that many approaches we can take here might be considered just one of the possible implementations that will work for some use cases but prevent others from being implemented.

The most flexible offline implementation base on the concept of executors.

For Wora that is: https://github.com/morrys/wora/tree/master/packages/offline-first that is being used under the hood of Apollo and Relay implementations.

A similar concept can be found in other libraries like AppSync or Offix library https://offix.dev/docs/offix-scheduler-introduction

Scheduler/Offline-First base on the idea that the client itself should not actually care about the offline-online state - it only needs to cache the server state and never offline state. Offline state can be kept in separate stores that form the blocking queue. When having this separate offline state client can quickly end up with problems of the typical distributed storage system (Typical stuff that Git resolves like conflicts/merge or rebase of the data etc.)

This will have numerous benefits:

Schedulers can enqueue offline operations based on certain criteria (as being offline). This means that URQL will only need to expose methods to operate on OptimisticMutations only. Example how it is possible to do that today in Apollo 2.0: https://github.com/morrys/wora/blob/master/packages/apollo-offline/src/ApolloClientOffline.ts#L137-L146

We can then later connect wora cache-persist as storage and get fully-featured offline support that will be flexible and satisfy various the community without poluting robust implementation of the URQL

The challenge of checking network error/state is that in some cases of unreliable networking we might end up with duplicates, there is need to have single blocking queue or at least some blocking configuration like in wora offline-first to not cause strange situation for conflicts and data overrides.

kitten commented 4 years ago

We are currently checking whether we can reuse some storage adaptor code you already have here.

That being said this is kind of besides the point of this RFC (luckily) 😅 We already have a solid caching and persistence system. The persistence is already able to persist server-accurate state accordingly. Optimistic updates to the cache are separate and always have been. They’re quickly and easily invalidated automatically at the first server result (as they should be). To this point of caching, we don’t need a layer for scheduling;

so to sum this up, here’s a list of what we already have (although some of this isn’t documented as we’re waiting for additional features from this RFC)

So don’t worry about the basic persistence 😅 that’s off-topic here and basically done. We’re more interested in the hard part here of what we do in the face of network-errors.

Mutations can be partially optimistic, so part of the problem is that we’re defining what the user is likely to want to happen in the case of mutations failing, due to the client being offline. Apart from that our problems are mostly solved.

We’re mainly currently discussing what happens to the operation’s result when an optimistic mutation fails. One option is to deliver the optimistic result to the framework bindings immediately and rely on the user to only use optimistic results where it’s sensible.

The other option is to defer the result indefinitely for mutations during offline, or to even deliver the errored result itself. Which are not the favoured options right now 🙃

tl;dr: this is not about persistence of cache data and/or conflict resolution, but simply around the implementation of a full offline exchange 😇

wtrocki commented 4 years ago

but simply around the implementation of a full offline exchange 😇

Yes. That is why I suggested talking look on wora/offline-first as alternative I'm not sure about Wora but my initial offline implementation was using exchange and it had the same problem. We had been blocking requests but that prevented people from writing proper UI. Then we moved to return offline type of error from the exchange and trigger mutation again to get an optimistic response. This is very very hacky and not optimal - causes flickering etc.

We’re mainly currently discussing what happens to the operation’s result when an optimistic mutation fails. One option is to deliver the optimistic result to the framework bindings immediately and rely on the user to only use optimistic results where it’s sensible.

Forgive me this silly question. Not sure about internals here but when I tried that in Apollo it was adding optimistic results to the cache as result from server. Would users will be able to tell that they got an optimistic response? How this will be invalidated after page refresh?

kitten commented 4 years ago

I forgot to leave a follow-up comment here, but v3 is now shipping Offline Support (experimental for now) with all the strategies we've mentioned across comments and PRs. https://formidable.com/open-source/urql/docs/graphcache/offline/

wtrocki commented 4 years ago

Awesome!