Front-End Web & Mobile

New features that will enhance your Real-Time experience on AWS AppSync

AWS AppSync is a managed service that uses GraphQL to simplify application development by letting applications securely access, manipulate, and receive real-time updates from multiple data sources such as databases or APIs.

Taking advantage of GraphQL subscriptions to perform real-time operations, AppSync can push data to clients that choose to listen to specific events from the backend. This means that you can easily and effortlessly make any supported data source in AWS AppSync real-time with connection management handled automatically between the client and the service. A backend service can easily broadcast data to connected clients or clients can send data to other clients, depending on the use case. Real-time data, connections, scalability, fan-out and broadcasting are all handled by intelligent client libraries and AppSync, allowing you to focus on your application business use cases and requirements instead of dealing with the complex infrastructure to manage WebSockets connections at scale.

Whenever a client invokes a GraphQL subscription operation with the AWS AppSync SDK or the Amplify clients, a secure WebSocket connection is automatically established and managed by AppSync and will remain constantly connected to your backend allowing users to receive real-time data from any supported AppSync data source:

Today we’re launching enhancements to AppSync that will further optimize access to engaging applications requiring real-time updates, such as gaming leaderboards, social media apps, live streaming, interactive chatrooms, IoT dashboards, and many others.

From a protocol perspective, AppSync real-time is now available with pure WebSockets in addition to the existing MQTT over WebSockets implementation. Pure WebSockets will make it easier for applications using built-in platform clients such as those included with web browsers to connect to AppSync. Applications with custom client and runtime requirements just need to use subscriptions with the new Real-Time endpoint (wss://), queries and mutations can still use HTTP clients connecting to the GraphQL endpoint (https://):

$ aws appsync get-graphql-api --api-id example123456 
{
    "graphqlApi": {
        "name": "myNewRealTimeGraphQL-API",
        "authenticationType": "API_KEY",
        "tags": {},
        "apiId": "example123456",
        "uris": {
            "GRAPHQL": "https://xxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql",
            "REALTIME": "wss:// xxxxxxxxxxxx.appsync-realtime-api.us-west-2.amazonaws.com/graphql"
        },
        "arn": "arn:aws:appsync:us-west-2: xxxxxxxxxxxx:apis/xxxxxxxxxxxx"
    }
}

With the new protocol, the maximum payload size is increased from 128kb to 240kb, providing enhanced connection rates and broadcast rates, adding CloudWatch metrics for real-time operations from connected clients, and implementing selection set filtering for subscriptions.

The selection set represents the collection of fields you define to be returned in a GraphQL operation. For instance, take the following mutation:

mutation CreateMessage($input: CreateMessageInput!) {
  createMessage(input: $input) {
    id
    message
    createdAt
  }
}

The selection set informs the GraphQL API backend the fields id, message and createdAt should be returned after the mutation operation takes place to create a message. Now with AppSync you can have a subscription OnCreateMessage linked to the mutation CreateMessage above returning a different selection set. For instance, if subscribed clients are only interested in receiving the message itself in real-time and none of the other fields set by the mutation, with the new support for selection set filtering on subscriptions you just need to set the required field or fields in the selection set which will be honored by AppSync:

subscription OnCreateMessage {
  onCreateMessage {
    message
  }
}

When taking advantage of the new pure WebSockets protocol support in your client application, selection set filtering is now defined per client as each client can specify their own selection set. Previously with MQTT over WebSockets the selection set was defined only by the client triggering the mutation, which made all subscribed clients receive the same selection set of the mutation payload.

With the new selection set filtering support, in case you are using MQTT over WebSockets in your application today and plan to move to pure WebSockets, you’ll need to pay close attention on how the subscriptions’ selection sets are configured in your clients. For instance, if your app relied only on the mutation selection set to push data to subscribed clients but ignored configuring the associated subscription selection set with the required fields in your code, your app might experience some undesirable side effects when moving to the new protocol. From now on you will need to make sure the fields received in a real-time subscription operation are properly defined in the client subscription selection set. In the example above a subscription without the message field defined in its selection set would still return the message data with MQTT over WebSockets as the field is defined in the mutation. The same behavior won’t apply for pure WebSockets. Defining the proper fields in the subscription selection set is essential when using pure WebSockets as the subscription selection set now takes priority when subscribing to real-time data. In addition to that it’s important to notice in both scenarios/protocols the selection set in the subscription must be a subset of the selection set of the mutation. In the example above, if the message field wasn’t defined in the mutation selection set in the first place the subscription would return a null value for the field.

Furthermore with the new protocol support we’re also increasing visibility on WebSockets connections and GraphQL subscriptions activity with CloudWatch metrics on active connections and subscriptions, status, errors, message size and more. In one of our tests we managed to effortlessly reach 10 million active WebSocket connections on AppSync with plenty of room to scale to much more (this particular test was steadily increasing the number of connections at a certain rate intentionally until it reached 10.5M connected clients). This kind of client activity can now be easily tracked and visualized in the CloudWatch console (CloudWatch charges apply):

 

 

Taking it for a spin

You can try the new implementation in few minutes with a small sample app. To get started install the Amplify CLI following the instructions here. Then you’ll need to install a couple of tools and dependencies using NPM to create a boilerplate React app:

$ npm install create-react-app
$ create-react-app myRealtimeApp
$ cd myRealtimeApp
$ npm install @aws-amplify/core@next @aws-amplify/api@next @aws-amplify/pubsub@next bootstrap

Execute the following command to create an Amplify project in the React app folder:

$ amplify init

Add an AppSync GraphQL API with API Key for the API Authentication:

$ amplify add api

Follow the default options. When prompted with “Do you have an annotated GraphQL schema?”, select “NO”, then when prompted with “Do you want a guided schema creation?”, select “YES” and “Single object with fields”. Replace the sample schema with:

type Message @model {
    id: ID!
    message: String!
    createdAt: String
}

The schema defines the app data model and uses the GraphQL Transform directive @model to deploy a DynamoDB table to store messages, automatically configuring all CRUDL (Create-Read-Update-Delete-List) logic in a GraphQL API to interact with data.

Execute the following command to create the AWS AppSync API and DynamoDB table:

$ amplify push

While CloudFormation is deploying the cloud services into your AWS account, open the file src/App.js and replace its contents with:

import React, { Component } from "react";
import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";
import logo from "./logo.svg";
import Amplify from "@aws-amplify/core";
import "@aws-amplify/pubsub";
import API, { graphqlOperation } from "@aws-amplify/api";
import aws_exports from "./aws-exports"; 
Amplify.configure(aws_exports);

const createMessage = `mutation createMessage($message: String!){
    createMessage(input:{message:$message}) {
    __typename
    id
    message
    createdAt
    }
}
`;

const onCreateMessage = `subscription onCreateMessage {
    onCreateMessage {
    __typename
    message
    }
}`;

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "",
      value: "",
      display: false
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  async componentDidMount() {
    this.subscription = API.graphql(
      graphqlOperation(onCreateMessage)
    ).subscribe({
      next: event => {
        if (event){
          console.log("Subscription: " + JSON.stringify(event.value.data, null, 2));
          this.setState({ display: true });
          this.setState({ message: event.value.data.onCreateMessage.message });
        }
      }
    });
  }

  handleChange(event) {
    this.setState({ value: event.target.value });
  }

  async handleSubmit(event) {
    event.preventDefault();
    event.stopPropagation();
    const message = {
      id: "",
      message: this.state.value,
      createdAt: ""
    };
    const mutation = await API.graphql(
      graphqlOperation(createMessage, message)
    );
    console.log("Mutation: " + JSON.stringify(mutation.data, null, 2));
  }

  render() {
    return (
      <div className="App">
        <img src={logo} className="App-logo" alt="logo" />
        <div className="jumbotron jumbotron-fluid p-0">
          <h2 className="center">Broadcaster</h2>
        </div>
        <br />
        <div className="container">
          <form onSubmit={this.handleSubmit}>
            <div className="form-group">
              <input
                className="form-control form-control-lg"
                type="text"
                value={this.state.value}
                onChange={this.handleChange}
              />
            </div>
            <input
              type="submit"
              value="Submit"
              className="btn btn-primary"
            />
          </form>
        </div>
        <br />
        {this.state.display ? (
          <div className="container">
            <div className="card bg-success">
              <h3 className="card-text text-white p-2">
                {this.state.message}
              </h3>
            </div>
          </div>
        ) : null}
      </div>
    );
  }
}

export default App;

Now you’re ready to start the app:

$ amplify serve

You can open http://localhost:3000 in multiple browsers or multiple browser windows to test, confirming messages are broadcasted and pushed to multiple clients using pure WebSockets with few lines of code:

 

Opening the Developer Tools in the browser and sending a message in the App by clicking the button Submit, you’ll notice in the Console section the mutation is sending data on all 3 fields (id and createdAt are automatically generated by the AppSync resolver) however the selection set in the subscription is configured to return only the message field:

Mutation: {
     "createMessage": {
         "__typename": "Message",
         "id": "be6eba3a-321e-468a-b278-31ab5c21c29b",
         "message": "My 1st Pure WebSockets message",
         "createdAt": "2019-11-12T08:00:00.000Z"
     }
}

Subscription: {
    "onCreateMessage": {
        "__typename": "Message",
        "message": "My 1st Pure WebSockets message"
    }
}

As mentioned before, with MQTT over WebSockets there is no selection set filtering on subscriptions: all fields sent on mutations are always reflected in the subscriptions for all clients. With pure WebSockets selection set filtering is now done per client as each client can define their own selection set.

Still in the browser developer tools, go to the Network tab. You’ll notice the WebSockets traffic is reaching a single secure endpoint (wss://API_ID.appsync-realtime-api.REGION.amazonaws.com/graphql):

Now developers can easily see and interact with the WebSockets messages and overall activity in the browser developer tools.

Conclusion

As of today, all existing and new AWS AppSync APIs will support both the implementation with MQTT over WebSockets as well as the new pure WebSockets protocol. While MQTT over WebSockets is available, the new protocol is our recommended option moving forward as it supports all the new features released today:

In order to take advantage of these features clients need to use the latest AppSync SDK for JavaScript and iOS (version 3.0.0+) and Amplify clients (version 2.1.0+) rolling out today, with the AppSync SDK for Android rolling out in the next week. The new clients will connect using pure WebSockets by default. Existing clients will keep working as usual with subscriptions via MQTT over Websockets with no impact to their application. Both existing and new clients will be able to send and receive real-time data using GraphQL subscriptions and communicate with each other in the same API independently of the protocol, no action is required from the developer perspective and no client code changes are necessary as existing clients won’t be affected.

With the new features AppSync is enhancing real-time capabilities with larger payloads, a new protocol implementation, better metrics and selection set filtering to keep providing a flexible and robust service for any real-time application use case. If you are implementing real-time in your application with AWS AppSync, you can now take advantage of all these new WebSockets enhancements.

What else would you like to see in AWS AppSync real-time suscriptions? Let us know if you have any ideas, if so feel free to create a feature request in our GitHub repository. Our team constantly monitors the repository and we’re always interested on developer feedback. Go build (now with more real-time features)!