Search code examples
node.jsapollooffline-cachingaws-appsyncgraphql-subscriptions

How to efficiently sync Apollo's cache using subscriptions and AWS AppSync


I'm using aws-appsync in a Node.js client to keep a cached list of data items. This cache must be available at all times, including when not connected to the internet.

When my Node app starts, it calls a query which returns the entire list of items from the AppSync data source. This is cached by Apollo's cache storage, which allows future queries (using the same GraphQL query) to be made using only the cache.

The app also makes a subscription to the mutations which are able to modify the list on other clients. When an item in the list is changed, the new data is sent to the app. This can trigger the original query for the entire list to be re-fetched, thus keeping the cache up to date.

Fetching the entire list when only one item has changed is not efficient. How can I keep the cache up to date, while minimising the amount of data that has to be fetched on each change?

The solution must provide a single point to access cached data. This can either be a GraphQL query or access to the cache store directly. However, using results from multiple queries is not an option.


The Apollo documentation hints that this should be possible:

In some cases, just using [automatic store updates] is not enough for your application ... to update correctly. For example, if you want to add something to a list of objects without refetching the entire list ... Apollo Client cannot update existing queries for you.

The alternatives it suggests are refetching (essentially what I described above) and using an update callback to manually update the cached query results in the store.

Using update gives you full control over the cache, allowing you to make changes to your data model in response to a mutation in any way you like. update is the recommended way of updating the cache after a query.

However, here it is referring to mutations made by the same client, rather than syncing using between clients using subscriptions. The update callback option doesn't appear to be available to a subscription (which provides the updated item data) or a query (which could fetch the updated item data).


Solution

  • As long as your subscription includes the full resource that was added, it should be possible by reading from and writing to the cache directly. Let's assume we have a subscription like this one from the docs:

    const COMMENTS_SUBSCRIPTION = gql`
      subscription onCommentAdded {
        commentAdded {
          id
          content
        }
      }
    `;
    

    The Subscription component includes a onSubscriptionData prop, so we should be able to do something along these lines:

    <Subscription
      subscription={COMMENTS_SUBSCRIPTION}
      onSubscriptionData={({ client, subscriptionData: { data, error } }) => {
        if (!data) return
        const current = client.readQuery({ query: COMMENTS_QUERY })
        client.writeQuery({
          query: COMMENTS_QUERY,
          data: {
            comments: [...current.comments, data.commentAdded],
          },
        })
      }}
    />
    

    Or, if you're using plain JavaScript instead of React:

    const observable = client.subscribe({ query: COMMENTS_SUBSCRIPTION })
    observable.subscribe({
      next: (data) => {
        if (!data) return
        const current = client.readQuery({ query: COMMENTS_QUERY })
        client.writeQuery({
          query: COMMENTS_QUERY,
          data: {
            comments: [...current.comments, data.commentAdded],
          },
        })
      },
      complete: console.log,
      error: console.error
    })