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

How to achieve offline support? #424

Closed
paldepind opened this issue Jul 19, 2016 · 66 comments
Closed

How to achieve offline support? #424

paldepind opened this issue Jul 19, 2016 · 66 comments

Comments

@paldepind
Copy link

Is it possible to use Apollo together with a client side database to achieve offline support? I found #307 where some comments imply that it should be possible but without explaining how.

Ideally we'd like to persist the data that Apollo fetches to a local database and use that database instead of fetching from a remote when the client is offline.

@stubailo
Copy link
Contributor

You might be able to use something like this: https://github.com/rt2zz/redux-persist

The goal is that you can use some standard Redux offline library rather than needing something specifically for apollo, but I don't know if we have all of the necessary features for this. Can you list some of your requirements? I think all we need to do is write up a guide about offline use, and give a few examples, but I don't have a good understanding of what people need. For example:

  1. How do you know if you're online and offline?
  2. If you come online, do you want to refetch all data or keep using cached data?
  3. What are the requirements for offline operation? What do you expect to happen with mutations, for example?

@stubailo stubailo changed the title Apollo with client side database How to achieve offline support? Jul 19, 2016
@paldepind
Copy link
Author

Thank you for your response @stubailo.

redux-persist is great but I don't think it solves the problem that we have (I could be wrong though). One aspect is that you persist exactly what is in the store. That is fine as long as one is comfortable with keeping the entire persisted client site in memory. But for more complex needs I believe a proper client side database is needed (i.e. Realm, IndexedDB or Sqlite). This is necessary if you need to do efficient queries on the data (these would match queries supported by the GraphQL server).

We are building a mobile application with React Native and we need it to be functional offline. We would like to persist data that is fetched by Apollo. I.e. if a user looks at some fetched data it is persisted locally so that he can still access it if using the application offline later.

I don't understand Apollo very well at all. But what I imagined is something like client side resolvers similar to the server side resolvers in the GraphQL schema. These resolvers would be given the opportunity to retrieve requested data from the local database before the query is sent to the server.

We use React Native and check online/offline status with its NetInfo API. If the application comes online we ideally wan't to refetch all data that has changed since the user went offline. The GraphQL remote would of course have to support that as well. We'd also like to have all mutations that happened while offline sent to the remote. The mutations would also have to be saved persistently. A user might make changes offline, close the app, and then later go online and expect his changes to be sent to the server.

Of course handling online/offline applications is quite tricky. We don't expect Apollo to solve these problems but we're curious if we could actually implement what we want without Apollo getting in the way.

@stubailo
Copy link
Contributor

Honestly it seems pretty hard to implement resolution of arbitrary GraphQL queries on the client. I guess you could reimplement your entire schema against the client-side database, but then you would be quite limited in what kinds of server-side features you could use.

Can you give me more specific examples of different views in your app, what data could be in there, and what queries you would need to display?

I think Apollo Client can give you some basic offline functionality out of the box, but it only knows how to run queries it's seen before. Since GraphQL arguments don't have any semantic meaning (there is no built-in sorting, filtering, etc) it would be hard to run totally new queries in a generic way.

@dahjelle
Copy link
Contributor

Honestly it seems pretty hard to implement resolution of arbitrary GraphQL queries on the client. I guess you could reimplement your entire schema against the client-side database, but then you would be quite limited in what kinds of server-side features you could use.

(This is a bit off-topic from Apollo Client, but in the interest of discussing offline support…)

PouchDB is a client-side database that's nearly feature-equivalent to CouchDB on the server, and uses the CouchDB sync protocol to enable syncing between front- and back-ends. PouchDB can also act as a proxy to a CouchDB instance, so it wouldn't be unreasonable to write a bunch of GraphQL resolvers that would work on both Node.js and in the browser.

I expect some of the other client-side DBs could enable similar things, though, as far as I'm aware, PouchDB/CouchDB is the only one offering built-in sync mechanisms (with user-provided conflict resolution).

@stubailo
Copy link
Contributor

I agree that this is possible - a similar concept is achieved by meteor with MongoDB and minimongo as well. I'm just saying that graphql is not the right server technology for this kind of database synchronization.

If you have some way of synchronizing the database to the client then you can definitely run GraphQL queries against that with Apollo Client.

@stubailo
Copy link
Contributor

I'm just coming from the perspective where you are trying to query many different kinds of back end data sources and you can't sync them all into MongoDB or couchdb or similar.

@paldepind
Copy link
Author

Honestly it seems pretty hard to implement resolution of arbitrary GraphQL queries on the client. I guess you could reimplement your entire schema against the client-side database, but then you would be quite limited in what kinds of server-side features you could use.

The goal is not to implement the entire capabilities of the server client side. Only some subset that would give sufficient offline features in our application. I.e. the local resolvers would be given the chance to get the data locally but they are allowed to fail if the user is trying to do something that only works online.

I am aware of PouchDB. It is a great solution to creating offline first applications in many cases (I don't know why you'd use it with Apollo though). But for various reasons we can't use it. We are using an SQL database on the server among other things.

I think it would be really great if the different ways of implementing offline support with Apollo could be explained or sketched out in the documentation.

@stubailo
Copy link
Contributor

My current theory is that for at least some applications it will be sufficient to not have any resolvers on the client at all, and just use previously fetched query results stored in the Apollo Client cache (something that already works now out of the box).

Perhaps to start a deeper discussion we can talk about a case where that isn't enough? I think it will be a lot easier to think about specific needs rather than "offline" in general, which can be a very broad topic with lots of different tradeoffs.

@paldepind
Copy link
Author

I think the theory sounds accurate. Is there a way to make the client cache persistent?

Maybe one case where that isn't sufficient is if your server supports querying a list of something over a range and you'd like to support that offline as well with different variables. If you've previously queried parts of or the entire list it should be possible to persist those results and perform the query on that data.

@mquandalle
Copy link
Contributor

Perhaps to start a deeper discussion we can talk about a case where that isn't enough?

There is a lot of demand for a version of Wekan that would work offline, it should

  • be able to persist boards and cards data across refresh (either in localstorage, IndexedDB, or AsyncStorage in the case of react-native)
  • be able to resolve queries using this local cache
  • be able to do some mutations (“add a card”) and have it reflected in the local cache
  • keep a queue of unsynced mutations and send them to the server when an online connection resume
  • update the client stores with the server data on resume

This seems quite ambitious in term of requirements, but actually the Meteor/DDP/Minimongo/GroundDB stack is already there, and apart from the conflict resolutions on the server-side, it’s almost easy to implement clients that works perfectly offline. Therefore the question is: should the same offline rich-client experience be possible in Apollo as well?

@smolinari
Copy link
Contributor

smolinari commented Jul 20, 2016

This is a great discussion.

I was thinking, how about having a "syncing" query system built that isn't UI driven, but rather "offline system" driven, which can also fill Apollo's store (and automatically any browser persisted data system). In other words, data would be pre-fetched with a forward-looking algorithm, as if the user was working her way through the app. This basically would prime the store with supplemental data. Done smartly, it might even make any app using Apollo actually run much quicker. Possibly?

I do see there are still the disadvantages of syncing systems though. Don't get me wrong.... 😄 A lot more thought would need to be done on making sure all the readable data is accurate/ up-to-date.

For mutating, how about something like a "mutation stack", which stores any mutations made during offline operation. Once the device goes online again, the mutations in the mutation stack would be popped off (FIFO) and ran against the back-end. The results of the "proper" mutations would be final and updated in the store (and again, automatically in the browser persisted data system). An empty mutation stack with no errors, after the flush of all the mutations to the server is done, would mean the device is in sync.

One thing that might be ran into here is conflicts (a lot like with Git versioning). If someone changed data, while the other users was offline, it could be their own data is outdated. Boom.. Conflict.

There is still the big issue of syncing all of the data, as it might have changed on the server. This will probably need some sort of MVCC system, where the nodes and even objects must always have a "lastUpdated" or "version" property. If any data node has been updated since the last sync, any queries asking for those nodes or objects must be requeried. There would have to be a subsystem to do this version checking and updating, which isn't really part of GQL. It might also have to be ran, before the mutation stack is flushed....

Hmm.....

Just rambling.....I have no idea if it makes sense or is at all feasible or not. LOL!

Scott

@Akryum
Copy link

Akryum commented Jul 22, 2016

My current theory is that for at least some applications it will be sufficient to not have any resolvers on the client at all, and just use previously fetched query results stored in the Apollo Client cache (something that already works now out of the box).

Is there any documentation already available about how to do just that?

@paynecodes
Copy link

paynecodes commented Aug 24, 2016

I think @Akryum's question is a good one. I understand that Apollo does this sort of thing in memory, and that is documented. I don't understand how the previously fetched queries could be persisted and retrieved between application restarts (think LocalStorage/AsyncStorage) by looking at the existing documentation. @stubailo, could you elaborate?

@tonyxiao
Copy link

tonyxiao commented Sep 6, 2016

Are there examples where people are using apollo in offline/online environments today?

@acamarata
Copy link

Does Apollo Client have any roadmap to have offline capability and features close to what current Meteor Client has via minimongo, etc.?

@stubailo
Copy link
Contributor

stubailo commented Oct 2, 2016

Not at the moment, no - advanced offline support isn't a priority for us right now. Meteor and minimongo is a much better approach for applications which have sophisticated realtime requirements, but at the cost of relying on a specific database.

@abcd-ca
Copy link

abcd-ca commented Nov 19, 2016

Just wanted to add my 2¢ about why I need persistence in my app: My app is currently built in Meteor with Ionic Framework on mobile devices. My users have a job to do out in the forest where a data connection is often weak or non-existent. When online, they must sync to get the latest version of the collections they'll need, then they go out into the mountains and do a bunch of additional data collection. Then they return to a connected area and the data syncs. Offline persistence is critical so they don't lose their work in the event of a crash or by accidentally quitting the app. (I also have an app coming up with a similar use case except the users will be inside a building with a wifi-only iPad where the building doesn't have any wifi).

Meteor is especially nice the way it handles the online/offline situation. With a very weak connection, the connection is frequently lost and regained. Prior to meteor I would have had to monitor network connectivity, make a REST request to sync when connectivity was restored and manage retrying if it failed. Meteor just magically syncs when it can and my app doesn't die when the sync fails, it just catches up later. It really removes a big chunk of complexity. However, minimogo is volatile (non-persistent). I've had some luck with the ground:db package but it seems to be perpetually in alpha – and now people are migrating to Apollo so I think persistence with Apollo is salient – especially on mobile. Exciting times!

@bryanerayner
Copy link

Falcor has a really elegant model of caching that I believe could work really well with something like GraphQL / Apollo. The only problem is that using it currently requires 100% buy-in. If there were a way to only use JSONGraph (it provides good cache invalidation support, error support, etc. at the data storage level), and Falcor's Model, but using GraphQL for the network transport, you'd have a winning solution in my books.

@abcd-ca
Copy link

abcd-ca commented Feb 27, 2017

Already impressed with Apollo/GraphQL – I've started using it. Still very interested about this offline/re-sync topic

@fongandrew
Copy link

So, just to clarify, if all we need is persistence of previous cached queries, then we basically just use something like redux-persist on an existing Redux store that we then hook up the Apollo client to? That is, maybe something like this:

import {compose, combineReducers, applyMiddleware, createStore} from 'redux';
import {persistStore, autoRehydrate} from 'redux-persist';
import { ApolloClient, ApolloProvider } from 'react-apollo';

// Init Apollo client
const client = new ApolloClient({ /* your opts here */ });

// add `autoRehydrate` as an enhancer to your store (note: `autoRehydrate` is not a middleware)
const store = createStore(
  combineReducers({
    apollo: client.reducer(),
    /* Any other Redux reducers you have go here */
  }),
  {}, // initial state
  compose(
    applyMiddleware(...),
    autoRehydrate()
  )
);

// begin periodically persisting the store
persistStore(store);

Probably need to also add some logic to sanity check the persisted data as well, especially if you're changing schemas and whatnot.

Caveat: I'm currently just researching how feasible it would be to implement offline storage with Apollo -- I actually haven't tested the above code yet.

@smolinari
Copy link
Contributor

@fongandrew - If you have success, do report here about it please! 😄

Scott

@funkyeah
Copy link

Congrats on the 1.0 for apollo client. Of course I'm particularly interested in the what's next section. Specifically "next-generation features like cache invalidation, persistent offline caching" which seems particularly relevant to this issue.

My use case is pretty much the same as @mquandalle in his comment, so I don't have a lot to add there, but I did want to point to a relatively new project takes a stab at addressing the primary needs I have but at the lower redux level https://github.com/jevakallio/redux-offline. I couldn't help but think when reading through the docs that it could help inform whatever solution ends up in Apollo.

@pi0
Copy link

pi0 commented Apr 3, 2017

@fongandrew Thanks for your starting point, here is a semi-working version.

Notes

  • It fails loading from local cache sometimes on an unknown race-condition but we still have offline support
  • During re-hydration process data.loading is not valid, we have to double check data contents is valid and if not valid still show loading
  • Query fetchPolicy should be cache-and-network (not tested with other variants)
import {compose, combineReducers, applyMiddleware, createStore} from 'redux';
import {persistStore, autoRehydrate} from 'redux-persist';
import { ApolloClient, ApolloProvider } from 'react-apollo';
import {AsyncStorage} from 'react-native'

// Persist Apollo State
// https://github.com/apollographql/apollo-client/issues/424
// https://github.com/rt2zz/redux-persist
const store = createStore(
    combineReducers({
        apollo: apolloClient.reducer(),
    }),
    {/* Initial state */},
    compose(
        applyMiddleware(apolloClient.middleware()),
        autoRehydrate()
    )
);

// Begin periodically persisting the store
persistStore(store, {
    storage: AsyncStorage
});

@stale
Copy link

stale bot commented Sep 19, 2017

This issue has been automatically closed because it has not had recent activity after being marked as stale. If you belive this issue is still a problem or should be reopened, please reopen it! Thank you for your contributions to Apollo Client!

@danieljvdm
Copy link
Contributor

danieljvdm commented Oct 31, 2017

I think this issue has become relevant again since Apollo 2.0 has moved from Redux.

I was using redux-persist before to manage offline/saved state on my React Native app but now obviously can't. I have a working solution but I think it can probably be optimized a bit. Here's what I'm doing:

I created an "afterware" or "link" that simply forwards the operation on but also extracts and saves the cache.

const storeUpdatedLink = new ApolloLink((operation, forward) => {
  const newCache = cache.extract();
  setApolloCache(newCache);
  return forward(operation);
});

setApolloCache is a call to AsyncStorage in which I JSON.stringify the cache and save it.

In my App.js in componentWillMount where the redux persist call used to be (still is since I'm using redux for other stuff) I use a Promise.all on the redux persist rehydration call and the apollo client AsyncStorage fetch:

Promise.all([
  persistStorePromise(store, { storage: AsyncStorage, blacklist: ['nav'] }),
  getApolloCache(),
]).then((val) => {
  this.client = createApolloClient(val[1]);
  this.setState({ rehydrated: true });
});

And then simply:

render() {
    if (!this.state.rehydrated) {
      return null;
    }
    return (
      <ApolloProvider client={this.client}>
        <Provider store={store}>
          <MyApp />
        </Provider>
      </ApolloProvider>
    );
  }

This actually outperforms redux persist on app launch from my benchmarks but I'm worried about the link that I had to make to know when the cache updates. It seems a bit hacky and I think it's actually middleware rather than afterware as it seems to lag one request behind. Does anyone have any ideas of how to improve this, specifically the persisting aspect?

@2WheelCoder
Copy link

2WheelCoder commented Nov 1, 2017

@danieljvdm I think your persisted cache is lagging one request behind because forward(operation) is called after you call cache.extract(). I haven't tested this yet, but this should solve it:

const storeUpdatedLink = new ApolloLink((operation, forward) => {
  const next = forward(operation);
  const newCache = cache.extract();
  setApolloCache(newCache);
  return next;
});

Thanks for taking a first stab at this. I think there is a lot of potential for some great offline patterns to build around Apollo 2.

EDIT

@danieljvdm Actually, I'm mistaken. The above is how redux middleware works, but because forward(operation) returns an observable, you need to wait for that observable to resolve before you extract the cache and persist it if you want to avoid missing your latest call.

const storeUpdatedLink = new ApolloLink((operation, forward) => {
  const observer = forward(operation);
  observer.subscribe({
    success: () => {
      const newCache = cache.extract();
      setApolloCache(newCache);
    }
  });
  return observer;
});

Or, something like that. When I implement myself I'll update. The docs on Stateless Links have some examples for using middleware similar to this.

@danieljvdm
Copy link
Contributor

Since this issue is closed and there's a new opened one reference above I'll continue the discussion there.

@chmac
Copy link
Contributor

chmac commented Nov 20, 2017

@2WheelCoder / @danieljvdm I've tried a similarly naive approach to persist the cache which appears to have been working for at least the last 5 minutes!

const CACHE_KEY = "@APOLLO_OFFLINE_CACHE";

class ApolloOfflineCache extends InMemoryCache {
  constructor(...args) {
    super(...args);

    // After creation, read the cache back from AsyncStorage and restore it
    Promise.resolve(true)
      .then(() => AsyncStorage.getItem(CACHE_KEY))
      .then(cacheJson => {
        this.restore(JSON.parse(cacheJson));
      })
      .catch(error => {
        console.error("ApolloOfflineCache restore error", error);
      });
  }

  saveToAsyncStorage() {
    Promise.resolve(true)
      .then(() =>
        AsyncStorage.setItem(CACHE_KEY, JSON.stringify(this.extract()))
      )
      .catch(error => {
        console.error("ApolloOfflineCache.saveToAsyncStorage error", error);
      });
  }

  broadcastWatches() {
    super.broadcastWatches();

    this.saveToAsyncStorage();
  }
}

It's pretty ugly, relies on broadcastWatches() which is a private, internal method of apollo-cache-memory, but seems to be getting the job done, and doesn't rely on any timing hacks, etc.

I'm using ApolloOfflineCache in my client instead of InMemoryCache.

EDIT: Cleaned up the example a bit, removed flow types. This is all inside react-native, so I have import { AsyncStorage } from "react-native"; also.

@danieljvdm
Copy link
Contributor

this seems pretty cool @chmac, do you have any more detail about how broadcastWatches is supposed to behave? Does it fire deterministically after store updates?

@chmac
Copy link
Contributor

chmac commented Nov 20, 2017

@danieljvdm As I read the InMemoryCache code, it's the internal mechanism used to propagate changes to any registered "watchers" on the cache. At first I started out overriding the write() method, then I saw writeFragment() and writeQuery() and started to realise some other methods also mutate data. To the best of my understanding (which is admittedly quite limited) the broadcastWatches() function is called in all of those places, after the cache write has finished.

I believe that the callbacks from broadcastWatches() are called synchronously, so in theory my saving of the cache only happens after the UI has updated, etc.

@dotansimha
Copy link
Contributor

@chmac your solution is good, but using the Observable from apollo-client will cause it to be Hot Observable before time, and causing the mutations to execute twice.

Should be something like that:

  const observer = Observable.from(forward(operation)).publishReplay(1).refCount();

  observer.subscribe({
    complete: () => {
      const newCache = cache.extract(true);
      // save it somewhere...
    },
  });

  return observer;

@fermuch
Copy link

fermuch commented Nov 21, 2017

I love to see your workarounds in here!!
I've been using something close to the one described by @danieljvdm, but I'm wondering: how do you handle cache purging?

AFAIK, InMemoryCache doesn't purge anything, since in the next run it'll be totally fresh, but having an ever-growing offline cache might not be a good idea 😛

@danieljvdm
Copy link
Contributor

@dotansimha were you referring to my code rather than @chmac 's?

@dotansimha
Copy link
Contributor

@danieljvdm yeah, you are right...

@danieljvdm
Copy link
Contributor

Ok thanks for the feedback, good to know...

@danieljvdm
Copy link
Contributor

Though those side effects don't seem to be happening or have happened, my understanding of observables is not the best so i'll take the necessary precautions. Though I think I may try move over to @chmac 's solution.

@jbaxleyiii
Copy link
Contributor

@chmac would you be open to writing a recipe for the docs for this?! @peggyrayzis this is pretty great!

@jbaxleyiii jbaxleyiii reopened this Nov 21, 2017
@chmac
Copy link
Contributor

chmac commented Nov 21, 2017

@jbaxleyiii Sure. I thought about wiring it up into an npm package, but there's quite a lot of assumptions baked into my implementation (the cache key name, the cache engine, when to repopulate the cache, etc), and I'm not sure how to best generalise those. Certainly happy to write something that points folks in this direction. Can you point me at an existing recipe or two that I can fork, etc?

@jbaxleyiii
Copy link
Contributor

@danieljvdm
Copy link
Contributor

@chmac I'd probably try emulate what redux-persist does with the abstraction of the cache engines. I'd say just start off with support for AsyncStorage + LocalStorage and go from there. This can be passed in the constructor with perhaps an optional argument for a cache key (otherwise it can be generated).

I'd be happy to collab with you on the PR if you'd like.

@ChildishDanbino
Copy link

ChildishDanbino commented Dec 5, 2017

I'd like to leave my thoughts even though what I found might be outdated with Apollo-Client 2.0. The app we are building needed to work fully offline in a flow-based manner. (Create an Order, add some items, submit changes when back online). We started off with Apollo-Client 1.0 right out of the box but found it extremely hard to find where data was located in the cache and write directly into the cache (creating a fake order offline to be synced when back online.. and having the user see that an order was created on device). In the end we solved this issue but taking a step back and using our own middleware to use Apollo-Client / Apollo-Server but also have more control over the data. I've open sourced the middleware here: https://github.com/Daadler6/apollo-thunk

When offline we could queue this mutations in an "offline" slice of the redux store like so

const offlineAction = () => this.props.ApolloMutation(variables);
this.props.pushQueue(offlineAction);

When reconnected we unload them like so:

 nextProps.offlineQueue.forEach((offlineAction) => {
          offlineAction();
        });

On top of that we used Redux-Persist for store rehydration and SQLite from Expo to do something other things as well as moving as much logic as we could onto the graph layer so in essence all the client needs to track / send is a list of changes that occurred offline. The graph will handle the creation / updating from there based on some properties.

I hope at least one person will find this useful as it took about two months to fully get the app working how we wanted to. If anyone has questions I'd be happy to help.

@chmac
Copy link
Contributor

chmac commented Dec 5, 2017

@danieljvdm Since my naive attempts to hack InMemoryCache, we had to dump apollo for most of our use cases, it was crashing on some union queries (while request-graphql works flawlessly). We are still using apollo in some places, but we're no longer trying to get it to handle caching. The complexity of the entire stack is just not worth it for our project. request-graphql + normalizr + redux works well for our offline needs.

@jamesreggio
Copy link
Contributor

Just leaving a note here that apollo-cache-persist provides a robust implementation of the concepts outlined by @danieljvdm and @chmac above.

It basically extracts the cache data and persists it on a configurable basis, and then restore it at startup time.

@Igosuki
Copy link

Igosuki commented Jul 27, 2018

What about implementing scuttlebut ? https://ssbc.github.io/secure-scuttlebutt/

@hwillson
Copy link
Member

hwillson commented Jul 27, 2018

To help provide a more clear separation between feature requests / discussions and bugs, and to help clean up the feature request / discussion backlog, Apollo Client feature requests / discussions are now being managed under the https://github.com/apollographql/apollo-feature-requests repository.

Migrated to: apollographql/apollo-feature-requests#11

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests