VulcanJS / Vulcan

🌋 A toolkit to quickly build apps with React, GraphQL & Meteor
http://vulcanjs.org
MIT License
7.98k stars 1.88k forks source link

[Feature] Offline-friendly #1909

Open eric-burel opened 6 years ago

eric-burel commented 6 years ago

Hi, I am working on offline features. I know that it sounds weird for a real-time framework to be offline first, but this feature is more and more requested, especially for low connectivity app (mobile user, salesmen etc.). Real-time and offline is actually a good combination: the data loads whenever you are connected with no refresh, and when you lose connection its still there.

The bare minimum is to have a caching mechanism so that data are available on small connectivity losses.

apollo-cache-persist : not good for Vulcan 1.x but good for next versions of Vulcan, apollo-offline : not good

As proposed by @justinr1234, apollo-cache-persist seems to be a good solution, but is meant for Apollo 2.0 only. Is Vulcan meant to include Apollo 2.0 in the versions to come ?

While we are using Apollo-client 1.x, that rely on redux we could use Malpaux/apollo-offline but I did not manage to set it up.

The problem is that the lib does not seem very happy with the existing hydratation process and SSR. I got some issue about state.offline being unexpected / undefined:

Unexpected keys "offline", "rehydrated" found in preloadedState argument passed to createStore. Expected to find one of the known reducer keys instead: "messages", "apollo". Unexpected keys will be ignored.
// happens because `state.offline` is not defined
Uncaught TypeError: Cannot read property 'outbox' of undefined

redux-persist : good?

It does not seem to crash blatantly. The localStorage is filled.

I also wrote a rehydratation callback for testing purposes, however I am not sure this hook will be actuallly called when I need it to to support offline mode (e.g on any page reload).

addCallback('router.client.rehydrate', ({ store }) => {
  const { persistor } = getRenderContext();
  console.log('rehydrating here', store.getState(), persistor.getState())
})

in the vulcan:core/modules/callbacks.js file. It is called correctly but I can't tell how to actually rehydrate. persistor.getState() looks like this:

{
registry: ["apollo"],
boostraped: false
}

Here is how it is created in vulcan:lib/modules/redux.js:

const persistConfig = {
  key: 'root',
  storage,
}

// create store, and implement reload function
export const configureStore = (reducers, initialState = {}, middlewares) => {
  let getReducers;
  if (typeof reducers === 'function') {
    getReducers = reducers;
    reducers = getReducers();
  }
  reducers = typeof reducers === 'object' ? combineReducers(reducers) : reducers;
  if (Meteor.isClient) reducers = persistReducer(persistConfig, reducers)

  middlewares = Array.isArray(middlewares) ? middlewares : [middlewares];

  const store = createStore(
    // reducers
    reducers,
    // initial state
    initialState,
    // middlewares
    compose(
      applyMiddleware(...middlewares),
      typeof window !== 'undefined' && window.devToolsExtension ? window.devToolsExtension() : f => f,
    ),
  );
  const persistor = persistStore(store)

  store.reload = function reload(options = {}) {
    if (typeof options.reducers === 'function') {
      getReducers = options.reducers;
      options.reducers = undefined;
    }
    if (!options.reducers && getReducers) {
      options.reducers = getReducers();
    }
    if (options.reducers) {
      reducers = typeof options.reducers === 'object' ? combineReducers(options.reducers) : options.reducers;
    }

    if (Meteor.isClient) reducers = persistReducer(persistConfig, reducers)
    this.replaceReducer(reducers);
    return store;
  };

  if (Meteor.isServer) return store
  return { store, persistor };
};

Here again I differentiate server and client, since I don't want persistance on the client. I changed the getRenderContext and routing.jsx to match this new behaviour (simply get {store, persistor} instead of just the store).

I am not sure whether redux-persist will automatically hydrate or wait for me to do some actions. I am not even sure it actually works, because I have no "true" offline mode, the app is not cached so if reload it while offline it simply crash. But it seems to work right now, so I think this is the way to go at first.

forms : maybe

Did not test it yet but my first idea is simply to disable the SmartForm whenever the app goes offline, and tell the user to wait for the app to go online. Same for forms-upload.

We could also cache the data in a queue and send the form when the app is reconnected but I am not fond of this pattern. The user might think he sent the data while it didn't and close the app before the reconnection happens, thus losing the form data he anted to send.

I propose to track offline related feature here in one place. This might not need to actual change in Vulcan, but adding offline support could be a recipee in the doc.

SachaG commented 6 years ago

Whatever solution we choose, we should definitely pick one that works with Apollo 2.x, since we're going to upgrade at some point.

Ideally every query/mutation would be cached locally until it can be synced back to the server, which would mean we wouldn't need to disable forms. But I don't know if that's supported by Apollo's offline solutions?

justinr1234 commented 6 years ago

Whatever is done should follow the lead of AppSync’s architecture as they used Apollo 2 and have built a paid product around it:

https://dev-blog.apollodata.com/aws-appsync-powered-by-apollo-df61eb706183

SachaG commented 6 years ago

I'm in touch with the AppSync people so I could ask them if you have any questions about how they handle offline usage.

eric-burel commented 6 years ago

Nice, yes if you plan to update to Apollo 2.x quickly the redux-persist way will be soon obsolete, at least people that are maintaining legacy apps will have some feedback about this.

To serve the app itself Meteor rely on their custom AppCache package or so I understood, since Service Workers are not yet widespread. I guess this would also be the correct solution for Vulcan.

justinr1234 commented 6 years ago

You need a service worker for offline loading

jacobpdq commented 6 years ago

Bit of research:

Gatsby uses sw-precache And there are year-old threads for meteor here

Discordius commented 6 years ago

I've looked into this a bunch, and in general it seems that we would want to go the way of service workers and generally making Vulcan by default a fully progressive web app.

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

eric-burel commented 5 years ago

Bad bot, this is still relevant. We could take care of this after the apollo2 update.

Zenoo commented 4 years ago

Is there any progress on this?

eric-burel commented 4 years ago

Not yet, I don't know how offline management evolved with Apollo2 and we are still focused on improving existing features. We'd be very glad to have external contributions on this, I don't think it's complicated but it requires to list existing solutions.

Zenoo commented 4 years ago

Here's an issue referencing offline support: https://github.com/apollographql/apollo-feature-requests/issues/11

They're recommending Offix: https://github.com/aerogear/offix/

justinr1234 commented 4 years ago

Offline is very complicated. This would take a very intimate knowledge of Vulcan.