Part 8 from series: How to build a real-time chat app

GraphQL Subscriptions

Create a GraphQL Subscription to send real-time message notifications

We can retrieve a list of messages and create messages with our GraphQL API from our Angular app. Now we need a way of subscribing and instantly notifying all apps of when a message gets sent using a subscription.

We will use GraphQL Subscriptions to achieve this, which use websockets under the hood.

Add the subscription to our schema

We're going to add a Subscription to our schema to describe events we're interested in such as when a new message has been added.

Within your project folder, open the file web-api/index.js and add these new lines within middle of the document to the typDefs constant:

File path icon web-api/index.js
// Existing code...

const typeDefs = gql`
  # Existing code...

  type Mutation {
    # Existing code...
  }

  type Subscription { # <-- Add new subscription here
    messageAdded: Message
  }

  ${messageGQL}
  ${userGQL}
`;

// Existing code...

Next we need to create a context function that will resolve data related to our websocket connection context such as user data and data sources that can be used in our subscription resolvers. We need to configure our ApolloServer and add the following code after the playground property:

File path icon web-api/index.js
// Existing code...

const server = new ApolloServer({
  // Existing code...
  playground: {
    // Existing code...
  },
  context: ({ req, connection }) => { // <-- Add new method
    if(connection) {
      return {
        ...connection.context,
        dataSources
      };
    }
  }
});

This will allow our resolvers to work with existing data source instances. We also need to configure the server instance to setup handling of subscriptions. Add the following line after the new ApolloServer.

File path icon web-api/index.js
// Existing code...

const server = new ApolloServer({
  // Existing code...
});

server.installSubscriptionHandlers(httpServer); // <-- Add new code

// Existing code...

Connect to Slack's real-time-messaging API (RTM)

Before we can create our subscription, we first need to make a connection to Slack's real-time messaging API. We will use the Node Slack SDK again for this.

Open the file web-api/slack/connection.js, then add the new RTMClient from the @slack/client module and PubSub dependency from apollo-angular to create a new instance.

File path icon web-api/slack/connection.js
const { WebClient, RTMClient /* <-- Add new import */ } = require('@slack/client');
const { PubSub } = require('apollo-server'); // <-- Add new import

let webAPI, rtmAPI; // <-- Define new variable

const pubsub = new PubSub(); // <-- Create new instance

// Existing code...

The PubSub module will give us a shared instance of an event generator. This will allow us to publish events to any subscribed clients any time an event of interest happens.

When Slack receives a message it will notify our GraphQL API through the use of the RTMClient instance, we will then need to notify all Angular apps connected to our API. This is done by publishing an event through the PubSub instance. GraphQL will use the PubSub instance to publish the schema event messageAdded with the new message data.

Next we need to redefine the connect function to create a memoized connection to Slack's real-time messaging API that we can reuse in our new subscriptions.

File path icon web-api/slack/connection.js
// Existing code...

function connect({ webToken, rtmToken }) { // <-- Destructure new token
  console.log('webToken', webToken);
  console.log('rtmToken', rtmToken);

  webAPI = new WebClient(webToken);
  rtmAPI = new RTMClient(rtmToken); // <-- Create new RTMClient instance with new token

  rtmAPI.start(); // <-- Boot RTMClient
}

function getConnection() {
  // Existing code...
}

// Existing code...

In order to retrieve a reference to our new connection with Slack from our subscriptions, we need to redefine the getConnection function to return the new RTMClient instance and also export the new shared PubSub instance.

File path icon web-api/slack/connection.js
// Existing code...

function connect({ webToken, rtmToken }) {
  // Existing code...
}

function getConnection() {
  if(!webAPI || !rtmAPI /* <-- Check connection exists */) {
    throw Error('Slack connections not found');
  }

  return { webAPI, rtmAPI /* <-- Return new reference */ };
}

module.exports = { connect, getConnection, pubsub };

We can now listen to new Slack notifications and publish new events of interest such as new sent messages within our subscriptions.

Create the Subscription resolver for Slack real-time messaging

Create and open the file web-api/api/slack-integration/resolvers/messages.messageAdded.subscription.js.

touch web-api/api/slack-integration/resolvers/messages.messageAdded.subscription.js

First we will need to import a few modules at the top of the file and define a couple of constants that will describe the connection.

File path icon web-api/api/slack-integration/resolvers/messages.messageAdded.subscription.js
const { pubsub, getConnection } = require('../../../slack/connection');
const slackMessageMap = require('../data-mappings/message');

const { rtmAPI: rtm } = getConnection();

const TOPIC = 'messageAdded';

Most of the imports should be familiar. The slackMessageMap we've used a couple of times previously. This will again be needed to transform Slack message data into a compatible model for when our schema publishes the data for a messageAdded event.

We get the real-time connection started with Slack (rtm) and define the TOPIC of messageAdded, which we declared in our schema to describe new events we're interested in.

We need to define an event handler that will handle the Slack response for when a new message is created. Then control when this method gets called by setting up a listener with the RTMClient on method.

File path icon web-api/api/slack-integration/resolvers/messages.messageAdded.subscription.js
// Existing code...

function onMessageHandler(messageData) { // <-- Add new handler function
  pubsub.publish(TOPIC, {
    [TOPIC]: slackMessageMap(messageData)
  });
}

rtm.on('message', onMessageHandler); // <-- Listen to Slack message events

The onMessageHandler function will get called when the Slack client emits a new message from the API. This will then call publish (in the handler) on the pubsub instance to publish a new messageAdded event.

For the second parameter to pubsub.publish, we define a payload object to hold message data with each new publication. This will map the message data sent from Slack to our schema compatible message model [TOPIC]: slackMessageMap(messageData).

After the handler, add the pubsub async iterator function to our module exports.

File path icon web-api/api/slack-integration/resolvers/messages.messageAdded.subscription.js
// Existing code...

module.exports = {
  subscribe: () => pubsub.asyncIterator(TOPIC)
};

The properties on the export function will plug in to our schema and allow our Angular app to connect to the asyncIterator over a websocket connection for real-time notifications. We just need to add our subscription resolver to our resolver export bundle.

Open the file web-api/api/slack-integration/resolvers/messages.resolvers.js and add the following lines:

File path icon web-api/api/slack-integration/resolvers/messages.resolvers.js
// Existing code...
const messageAdded = require('./messages.messageAdded.subscription'); // <-- Add the new module

module.exports = {
  // Existing code...

  Mutation: {
    // Existing code...
  },

  Subscription: { // <-- Add the new subscription resolver
    messageAdded
  },

  // Existing code...
};

We now have everything we need for our Angular app to connect to our GraphQL subscription.