Supporting offline functionality in your mobile app

About offline functionality

Your mobile app can run offline and allows users to query and create mutations using the @aerogear/voyager-client module.

All queries are performed against the cache, a mutation store (or offline store) supports offline mutations.

If a client goes offline for a long period of time, the mutation store negotiates local updates with the server using conflict resolution strategies.

When a client comes online again, the mutations are replicated back to the server.

RHMDS Data Sync 35 0919 online offline

Developers can attach listeners to get notifications about updates applied on the server or failing, and take appropriate actions.

Mutations and Local Cache

By default queries and the results of mutations are cached.

Mutations can change query results, make sure to call the refetchQueries or update options of the mutate method to ensure the local cache is kept up to date.

The @aerogear/voyager-client module also provides cache helper functions to reduce the amount of code required, as described in Using cache update helpers.

For more information about mutate and the options available, see Apollo’s document about mutations.

Creating an offline client

The @aerogear/voyager-client module provides an OfflineClient class which exposes the following functionality:

  • direct access to the mutation store

  • allows you to register multiple offline event listeners as described in Listening for events

  • automatically ensures the mobile app’s local cache is kept up to date. This client automatically generates update methods as described in Using cache update helpers.

To create the client:

import { OfflineClient } from '@aerogear/voyager-client';

let config = {
  httpUrl: "http://localhost:4000/graphql",
  wsUrl: "ws://localhost:4000/graphql",
}

async function setupClient() {

  let offlineClient = new OfflineClient(config);
  let client = await offlineClient.init();
}

setupClient();

This client can replace an Apollo client as it supports the same functionality.

Detecting mutations while offline

If a mutation occurs while the device is offline, the client.mutate function:

  • returns immediately

  • returns a promise with an error

You can check the error object to isolate errors related to the offline state. Invoking the watchOfflineChange() method on an error object, watches for when an offline change is synced with the server, and sends a notification when triggered.

For example:

  client.mutate(...).catch((error)=> {
    // 1. Detect if this was an offline error
   if(error.networkError && error.networkError.offline){
     const offlineError: OfflineError =  error.networkError;
     // 2. We can still track when offline change is going to be replicated.
     offlineError.watchOfflineChange().then(...)
   }
  });
In addition to watching individual mutations, you can add a global offline listener when creating a client as described in Listening for events.

Performing mutations while offline

The @aerogear/voyager-client module provides an offlineMutate method which extends Apollo’s mutate function with some extra functionality. This includes automatically adding some fields to each operation’s context.

To set up the offline client, see Creating an offline client.

Once set up is complete, offlineMutate is then available to use.

The offlineMutate method accepts the same parameters as mutate with some additional optional parameters also available.
  const { CacheOperation } = require('@aerogear/voyager-client');

  client.offlineMutate({
    ...
    updateQuery: GET_TASKS, (1)
    operationType: CacheOperation.ADD, (2)
    idField: "id", (3)
    returnType: "Task" (4)
    ...
  })
1 The query or queries which should be updated with the result of the mutation.
2 The type of operation being performed. Should be "add", "refresh" or "delete". Defaults to "add" if not provided.
3 The field on the object used to identify it. Defaults to "id" if not provided.
4 The type of object being operated on.

Supporting app restarts while offline

An Apollo client holds all mutation parameters in memory. An offline Apollo client continues to store mutation parameters and once online, it restores all mutations to memory. Any update functions that are supplied to mutations cannot be cached by an Apollo client resulting in the loss of all optimistic responses after a restart. Update functions supplied to mutations cannot be saved in the cache. As a result, all optimisticResponses disappear from the application after a restart and only reappear when the Apollo client becomes online and successfully syncs with the server.

To prevent the loss of all optimisticResponses after a restart, you can configure the Update Functions to restore all optimisticResponses.

const updateFunctions = {
  // Can contain update functions from each component
  ...ItemUpdates,
  ...TasksUpdates
}

let config = {
  mutationCacheUpdates: updateFunctions,
}

You can also use getUpdateFunction to automatically generate functions:

const { createMutationOptions, CacheOperation } = require('@aerogear/voyager-client');

const updateFunctions = {
  // Can contain update functions from each component
  createTask: getUpdateFunction({
      mutationName: 'createTask',
      idField: 'id',
      updateQuery: GET_TASKS,
      operationType: CacheOperation.ADD
    }),
  deleteTask: getUpdateFunction({
      mutationName: 'deleteTask',
      idField: 'id',
      updateQuery: GET_TASKS,
      operationType: CacheOperation.DELETE
    })
}

let config = {
  ...
  mutationCacheUpdates: updateFunctions,
  ...
}

Ensuring specified mutations are performed online only

If you wish to ensure certain mutations are only executed when the client is online, use the GraphQL directive @onlineOnly, for example:

exampleMutation(...) @onlineOnly {
  ...
}

Listening for events

To handle all notifications about offline related events, use the offlineQueueListener listener in the config object

The following events are emitted:

  • onOperationEnqueued - Called when new operation is being added to offline queue

  • onOperationSuccess - Called when back online and operation succeeds

  • onOperationFailure - Called when back online and operation fails with GraphQL error

  • queueCleared - Called when offline operation queue is cleared

You can use this listener to build User Interfaces that show pending changes.

Using cache update helpers

The @aerogear/voyager-client module provides an out of the box solution for managing updates to your application’s cache. It can intelligently generate cache update methods for both mutations and subscriptions.

Using cache update helpers for mutations

The following example shows how to use these helper methods for mutations. To use these methods, create an offline client as described in Creating an offline client and then use the offlineMutate method. The offlineMutate function accepts a MutationHelperOptions object as a parameter.

const { createMutationOptions, CacheOperation } = require('@aerogear/voyager-client');

const mutationOptions = {
  mutation: ADD_TASK,
  variables: {
    title: 'item title'
  },
  updateQuery: {
    query: GET_TASKS,
    variables: {
      filterBy: 'some filter'
    }
  },
  typeName: 'Task',
  operationType: CacheOperation.ADD,
  idField: 'id'
};

You can also provide more than one query to update the cache by providing an array to the updateQuery parameter:

const mutationOptions = {
  ...
  updateQuery: [
    { query: GET_TASKS, variables: {} }
  ]
  ,
  ...
};

The following example shows how to prepare an offline mutation to add a task using the mutationOptions object and how to update the GET_TASK query for the client’s cache.

const { createMutationOptions, CacheOperation } = require('@aerogear/voyager-client');

client.offlineMutate<Task>(mutationOptions);

If you do not want to use the offline client you can also use the createMutationOptions function directly. This function provides an Apollo compatible MutationOptions object to pass to your pre-existing client. The following example shows how to use this function where mutationOptions is the same object as the previous code example.

const options = createMutationOptions(mutationOptions);

client.mutate<Task>(options);

Using cache update helpers for subscriptions

The @aerogear/voyager-client module provides a subscription helper which can generate the necessary options to be used with Apollo Client’s subscribeToMore function.

To use this helper, we first need to create some options, for example:

const { CacheOperation } = require('@aerogear/voyager-client');

const options = {
  subscriptionQuery: TASK_ADDED_SUBSCRIPTION,
  cacheUpdateQuery: GET_TASKS,
  operationType: CacheOperation.ADD
}

This options object informs the subscription helper that for every data object received because of the TASK_ADDED_SUBSCRIPTION the GET_TASKS query should also be kept up to date in the cache.

You can then create the required cache update functions:

const { createSubscriptionOptions } = require('@aerogear/voyager-client');

const subscriptionOptions = createSubscriptionOptions(options);

To use this helper, pass this subscriptionOptions variable to the subscribeToMore function of our ObservableQuery.

const query = client.watchQuery<AllTasks>({
  query: GET_TASKS
});

query.subscribeToMore(subscriptionOptions);

The cache is kept up to date while automatically performing data deduplication.

Using cache update helpers for multiple subscriptions

The @aerogear/voyager-client module provides the ability to automatically call subscribeToMore on your ObservableQuery. This can be useful in a situation where you may have multiple subscriptions which can affect one single query. For example, if you have a TaskAdded, TaskDeleted, and a TaskUpdated subscription you require three separate subscribeToMore function calls. To avoid this, use the subscribeToMoreHelper function from the @aerogear/voyager-client module to automatically handle this by passing an array of subscriptions and their corresponding queries:

const { CacheOperation } = require('@aerogear/voyager-client');

const addOptions = {
  subscriptionQuery: TASK_ADDED_SUBSCRIPTION,
  cacheUpdateQuery: GET_TASKS,
  operationType: CacheOperation.ADD
}

const deleteOptions = {
  subscriptionQuery: TASK_DELETED_SUBSCRIPTION,
  cacheUpdateQuery: GET_TASKS,
  operationType: CacheOperation.DELETE
}

const updateOptions = {
  subscriptionQuery: TASK_UPDATED_SUBSCRIPTION,
  cacheUpdateQuery: GET_TASKS,
  operationType: CacheOperation.REFRESH
}

const query = client.watchQuery<AllTasks>({
  query: GET_TASKS
});

subscribeToMoreHelper(query, [addOptions, deleteOptions, updateOptions]);

Detecting Network Status

Use the NetworkStatus interface to check the current network status, or to register a listener which performs actions when the status of the network changes.

Two default implementations are provided:

  • WebNetworkStatus for web browsers

  • CordovaNetworkStatus for Cordova

The following example demonstrates how to register a listener using CordovaNetworkStatus:

import { CordovaNetworkStatus, NetworkInfo } from '@aerogear/voyager-client';
const networkStatus = new CordovaNetworkStatus();

networkStatus.onStatusChangeListener({
  onStatusChange: info => {
    const online = info.online;
    if (online) {
      //client is online, perform some actions
    } else {
      //client is offline
    }
  }
});

let config = {
  ...
  networkStatus: networkStatus,
  ...
};

//create a new client using the config