Advanced workflows
Delta Sync
DeltaSync allows you to perform automatic synchronization with an AWS AppSync GraphQL server. The client will perform reconnection, exponential backoff, and retries when network errors take place for simplified data replication to devices. It does this by taking the results of a GraphQL query and caching it in the local Apollo cache. The DeltaSync API manages writes to the Apollo cache for you, and all rendering in your app (such as from React components, Angular bindings) should be done through a read-only fetch.
In the most basic form, you can use a single query with the API to replicate the state from the backend to the client. This is referred to as a "Base Query" and could be a list operation for a GraphQL type which might correspond to a DynamoDB table. For large tables where the content changes frequently and devices switch between offline and online frequently as well, pulling all changes for every network reconnect can result in poor performance on the client. In these cases you can provide the client API a second query called the "Delta Query" which will be merged into the cache. When you do this the Base Query is run an initial time to hydrate the cache with data, and on each network reconnect the Delta Query is run to just get the changed data. The Base Query is also run on a regular bases as a "catch-up" mechanism. By default this is every 24 hours however you can make it more or less frequent.
By allowing clients to separate the base hydration of the cache using one query and incremental updates in another query, you can move the computation from your client application to the backend. This is substantially more efficient on the clients when regularly switching between online and offline states. This could be implemented in your AWS AppSync backend in different ways such as using a DynamoDB Query on an index along with a conditional expression. You can also leverage Pipeline Resolvers to partition your records to have the delta responses come from a second table acting as a journal. A full sample with CloudFormation is available in the AppSync documentation. The rest of this documentation will focus on the client usage.
You can also use Delta Sync functionality with GraphQL subscriptions, taking advantage of both only sending changes to the clients when they switch network connectivity but also when they are online. In this case you can pass a third query called the "Subscription Query" which is a standard GraphQL subscription statement. When the device is connected, these are processed as normal and the client API simply helps make setting up realtime data easy. However, when the device transitions from offline to online, to account for high velocity writes the client will execute the resubscription along with synchronization and message processing in the following order:
- Subscribe to any queries defined and store results in an incoming queue
- Run the appropriate query (If
baseRefreshIntervalInSeconds
has elapsed, run the Base Query otherwise only run the Delta Query) - Update the cache with results from the appropriate query
- Drain the subscription queue and continue processing as normal
Finally, you might have other queries which you wish to represent in your application other than the base cache hydration. For instance a getItem(id:ID)
or other specific query. If your alternative query corresponds to items which are already in the normalized cache, you can point them at these cache entries with the cacheUpdates
function which returns an array of queries and their variables. The DeltaSync client will then iterate through the items and populate a query entry for each item on your behalf. If you wish to use additional queries which don't correspond to items in your base query cache, you can always create another instance of the client.sync()
process.
Usage
// Start DeltaSyncconst subscription = client.sync(options)/*Under the covers, this is actually an Observable<T> that the AppSync client automatically subscribes to for you, so the returned object is a "subscription". This means that you can automatically stop the synchronization process like so:*/// Stop DeltaSyncsubscription.unsubscribe();
The options
object
baseQuery
query
: ADocumentNode
for the base data (e.g. as returned bygql
)variables
[optional]: An object with the query variables, if any.baseRefreshIntervalInSeconds
[optional]: Number of seconds after which the base query will be run again. Default value:86400
(24 hrs)update
[optional]: A function to update the cache, see: Apollo'supdate
function
subscriptionQuery
query
: ADocumentNode
for the subscription (e.g. as returned bygql
)variables
[optional]: An object with the query variables, if any.update
[optional]: A function to update the cache, see: Apollo'supdate
function
deltaQuery
query
: ADocumentNode
for the deltas (e.g. as returned bygql
)variables
[optional]: An object with the query variables, if any.update
[optional]: A function to update the cache, see: Apollo'supdate
function
The buildSync helper
The quickest way to get started with the DeltaSync feature is by using the buildSync
helper function. This helper function will build an options
object with the appropriate update
functions that will update the cache for you in a similar fashion to the offline helpers.
The first argument you need to pass is the GraphQL __typename
for your base query. The second argument is the options
object from the previous section (without the update
keys, since those will be generated for you by this helper function).
You can optionally pass a cacheUpdates
parameter to the second argument with the following structure:
- deltaRecord: A function which receives a
deltaRecord
(e.g. an individual item in the cache populated by the base/delta/subscription query) and returns an array of GraphQL queries and it's variables to be written to the cache.
Example:
client.sync( buildSync("Post", { baseQuery: { query: DeltaSync.BaseQuery }, subscriptionQuery: { query: DeltaSync.Subscription }, deltaQuery: { query: DeltaSync.DeltaSync }, cacheUpdates: ( deltaRecord ) => { const id = deltaRecord.id; return [{ query: DeltaSync.GetItem, variables: { id: id } }]; } }) )
Requirements for helper function
- Your
baseQuery
returns a list, not a nested type - Your
deltaQuery
expects a parameter calledlastSync
of typeAWSTimestamp
and returns a list with the same fields as yourbaseQuery
(an optionally, anaws_ds
field with a value of'DELETE'
for deletions, any other value for insert/update) - The mutations that trigger the subscription in your
subscriptionQuery
should return a single record with the same fields as the items from yourbaseQuery
, (an optionally, anaws_ds
field with a value of'DELETE'
for deletions, any other value for insert/update)
Example
The schema for this sample is below. A full sample with CloudFormation is available in the AppSync documentation.
input CreatePostInput { author: String! title: String! content: String! url: String ups: Int downs: Int}
enum DeltaAction { DELETE}
type Mutation { createPost(input: CreatePostInput!): Post updatePost(input: UpdatePostInput!): Post deletePost(id: ID!): Post}
type Post { id: ID! author: String! title: String! content: String! url: AWSURL ups: Int downs: Int createdDate: String aws_ds: DeltaAction}
type Query { getPost(id: ID!): Post listPosts: [Post] listPostsDelta(lastSync: AWSTimestamp): [Post]}
type Subscription { onDeltaPost: Post @aws_subscribe(mutations: ["createPost","updatePost","deletePost"])}
input UpdatePostInput { id: ID! author: String title: String content: String url: String ups: Int downs: Int}
schema { query: Query mutation: Mutation subscription: Subscription}
Sample queries
query Base { listPosts { id title author content }}
query Delta($lastSync: AWSTimestamp!) { listPostsDelta( lastSync: $lastSync ) { id title author content aws_ds }}
subscription Subscription { onDeltaPost { id title author content aws_ds }}
Define the queries from above in a ./graphql/DeltaSync.js
file to import in your app:
import gql from "graphql-tag";
export const BaseQuery = gql`query Base{ listPosts { id title author content }}`;
export const GetItem = gql`query GetItem($id: ID!){ getPost(id: $id) { id title author content }}`;
export const Subscription = gql`subscription Subscription { onDeltaPost { id title author content }}`;
export const DeltaSync = gql`query Delta($lastSync: AWSTimestamp!) { listPostsDelta( lastSync: $lastSync ) { id title author content aws_ds }}`;
import { AWSAppSyncClient, buildSync } from "aws-appsync";import * as DeltaSync from "./graphql/DeltaSync";
const client = new AWSAppSyncClient({ // ...});
const subscription = client.sync( buildSync('Post', { baseQuery: { query: DeltaSync.BaseQuery }, subscriptionQuery: { query: DeltaSync.Subscription }, deltaQuery: { query: DeltaSync.DeltaSync }, cacheUpdates : ({id}) => [{query: DeltaSync.getItem, variables: {id}] }));
React example
Suppose you have an app created with Create React App with the following structure:
- App.js
- Sets up
AWSAppSyncClient
andclient.sync
as above - Renders
<AllPosts />
and<SinglePost item={2}>
- Sets up
- AllPosts.jsx exports
<AllPosts />
- GetPost.jsx exports
<SinglePost item={id}>
App.js
const client = new AWSAppSyncClient({ url: awsconfig.aws_appsync_graphqlEndpoint, region: awsconfig.aws_appsync_region, auth: { type: awsconfig.aws_appsync_authenticationType, apiKey: awsconfig.aws_appsync_apiKey }});
client.hydrated().then(() => client.sync( buildSync("Post", { baseQuery: { query: DeltaSync.BaseQuery }, subscriptionQuery: { query: DeltaSync.Subscription }, deltaQuery: { query: DeltaSync.DeltaSync }, cacheUpdates: ({ id }) => [ { query: DeltaSync.GetItem, variables: { id } } ] }) ));
const App = () => ( <ApolloProvider client={client}> <Rehydrated> <div> <OnePost id="96d5e889-38ba-4846-84d0-a11d6447d34b" /> <hr /> <AllPosts /> </div> </Rehydrated> </ApolloProvider>);
In AllPosts.jsx
you would have code like so:
const AllPosts = ({ postsList }) => ( <div> <pre style={% raw %}{{ textAlign: "left" }}{% endraw %}> {JSON.stringify(postsList, null, 2)} </pre> </div>);
export default graphql(DeltaSync.BaseQuery, { options: { fetchPolicy: "cache-only" }, props: ({ data }) => ({ postsList: data.listPosts || [] })})(AllPosts);
In GetPost.jsx
you would have:
const OnePost = ({ post }) => ( <div> <pre style={% raw %}{{ textAlign: "left" }}{% endraw %}>{JSON.stringify(post, null, 2)}</pre> </div>);
export default graphql(DeltaSync.GetItem, { options: ({ id }) => ({ variables: { id }, fetchPolicy: "cache-only" }), props: ({ data: { getPost } }) => ({ post: getPost })})(OnePost);
Note: The fetchPolicy
is cache-only
as all of the network requests are handled automatically by the client.sync()
operation. You should use this if using different queries in other components as the client.sync()
API manages the cache lifecycle. If you use another fetch-policy
such as cache-and-network
then extra network requests may take place negating the Delta Sync benefits.
Writing update functions
If you do not want to use the buildSync
helper then you are responsible for managing cache updates in your application code. Note that this can be a complex process as you will need to manage create, update, and deletes appropriately. An example of this would be updating the cache with a delta record as below, noting that you must update the returned type to match the type from your base query.
client.sync({ baseQuery: { query: DeltaSyncQueries.BaseQuery }, deltaQuery: { query: DeltaSyncQueries.DeltaSync, update: (cache, { data: { listPostsDelta } }) => { const query = DeltaSyncQueries.GetItem;
listPostsDelta.forEach(deltaRecord => { const variables = { id: deltaRecord.id };
cache.writeQuery({ query, variables, data: { getPost: { ...deltaRecord, __typename: 'Post' } } }); }); } } });