DEV Community

Cover image for Beginner's guide to using Websockets in Apollo Client GraphQL Subscription
Sayan Mondal
Sayan Mondal

Posted on

Beginner's guide to using Websockets in Apollo Client GraphQL Subscription

GraphQL started to be a great option for writing customizable API’s and combining multiple services into one single endpoint.

The entire idea of having a single endpoint but getting different combinations of models is amazing, especially for companies which are working with large scale multi-platform applications with different frontend/mobile developers and designers.


What is a WebSocket?

Typically, WebSocket is a protocol which provides a way to exchange data between browsers and servers via a persistent connection.

It's very useful for applications that require continuous data exchange, eg- online games, real-time systems, trading engines, and so on.

The way we open a new WebSocket connection is by using a special ws protocol in our regular URL. For example:-

Regular URL: http://someurl.com
WebScoket URL: ws://someurl.com


What about wss?

Just like an HTTPS is more secure than any HTTP request! wss is an encrypted protocol just like HTTPS but for WebSockets.

wss is not only encrypted but also much more reliable. That’s because ws:// data is not encrypted, visible for any intermediary. Old proxy servers do not know about WebSocket, they may see “strange” headers and abort the connection.

On the other hand, wss:// is WebSocket over TLS, (same as HTTPS is HTTP over TLS), the transport security layer encrypts the data at sender and decrypts at the receiver. So data packets are passed encrypted through proxies. They can’t see what’s inside and let them through.


What is a GraphQL Subscription!

Like queries, Subscriptions are a way to fetch data. Unlike queries, subscriptions maintain an active connection to your GraphQL server via WebSocket. This enables your server to push updates to the subscription's result over time.

They are very useful to notify the client about any updates to the real-time requests. If the back-end data changes at any point, the client will immediately get notified through the already active server connection.


Where does Apollo Client come into the picture

The Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. It can be used to fetch, cache, and modify application data, all while automatically updating your UI.

The core @apollo/client library provides built-in integration with React, and the larger Apollo community maintains integrations for other popular view layers. Thus it comes in handy for developers looking to manage GraphQL data in the Front-End.

Defining a Subscriber

A Subscriber has to be defined both on the server as well as the client-side. However, in this blog, we are specifically going to talk about handling subscriber in the client-side since server-side subscriber configuration could vary based on your project requirements.

You could set up a subscriber server using goLang, Node JS, etc but the client-side handling would pretty much remain the same for different tools, libraries and frameworks.


In this blog, I'm going to discuss how to handle client-side subscription with Apollo using reference from an Open Source Project called LitmusChaos.

Litmus

Litmus is a toolset to do cloud-native chaos engineering. Litmus provides tools to orchestrate chaos on Kubernetes to help developers and SREs find weaknesses in their application deployments. Litmus can be used to run chaos experiments initially in the staging environment and eventually in production to find bugs, vulnerabilities.

The project is under active development as a Sandbox project with CNCF.

Our Subscriber Definition

Since Litmus is a project where we deal with a lot of Chaos Experiments and Workflow Data, we have set up our Subscriber such that we listen to the Workflow Events and retrieve a number of essential parameters which are required to plot Analytics Graph, Workflow Representation Data and so on.

const WORKFLOW_EVENTS = gql`
  subscription workflowEvents($projectID: String!) {
    workflowEventListener(project_id: $projectID) {
      workflow_id
      workflow_name
      workflow_run_id
      execution_data
      project_id
      cluster_name
      last_updated
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

When Apollo Client executes the workflowEvents subscription, it establishes a connection to our GraphQL server and listens for response data. Unlike a query, there is no expectation that the server will immediately process and return a response. Instead, our server only pushes data to the client when a particular event occurs on your backend (In our case, a new workflow is scheduled/ran).


Executing a Subscriber

Apollo provides us with different hooks like useQuery, useSubscription, etc to make our life easier when executing queries from a GraphQL server. We'll be executing a subscription we defined earlier to listen to the event and keep our frontend updated with any data changes from the backend once we are subscribed to that particular workflow.

Whenever a query returns a result in Apollo Client, that result includes a subscribeToMore function. You can use this function to execute a followup subscription that pushes updates to the query's original result.

const { subscribeToMore, data, error } = useQuery(
    WORKFLOW_DETAILS,
    {
      variables: { projectID: selectedProjectID },
      fetchPolicy: 'cache-and-network',
    }
  );

  // Using subscription to get realtime data

  subscribeToMore({
      document: WORKFLOW_EVENTS,
      variables: { projectID: selectedProjectID },
      updateQuery: (prev, { subscriptionData }) => {
        if (!subscriptionData.data) return prev;
        const modifiedWorkflows = prev.getWorkFlowRuns.slice();

        return { ...prev, getWorkFlowRuns: modifiedWorkflows };
      },
  });

Enter fullscreen mode Exit fullscreen mode

All this subscription is doing is query the data at the very beginning and then listening to the server for any more updates on the same Workflow Event, if new data is populated the subscriber updates the previous data with the new one.

The subscribeToMore function is similar in structure to the fetchMore function that's commonly used for handling pagination. The primary difference is that fetchMore executes a followup query, whereas subscribeToMore executes a subscription.


Setting up the transport

Because subscriptions maintain a persistent connection, they can't use the default HTTP transport that Apollo Client uses for queries and mutations. Instead, Apollo Client subscriptions most commonly communicate over WebSocket, via the community-maintained subscriptions-transport-ws library.

We'd require subscriptions-transport-ws since it handles GraphQL WebSocket server and client to facilitate GraphQL queries, mutations and subscriptions over WebSocket behind the hood, other than that we'd require the @apollo/client library which can basically handle all your apollo related tasks quite smoothly and it also eliminates the chance of having version collision when trying out hooks/features from other community-driven libraries.

npm install @apollo/client subscriptions-transport-ws
Enter fullscreen mode Exit fullscreen mode

Import and initialize a WebSocketLink object in the same project file where you initialize ApolloClient

import { WebSocketLink } from '@apollo/client/link/ws';

const wsLink = new WebSocketLink({
  uri: `ws:<GraphQL Endpoint>`,
  options: {
    reconnect: true,
    lazy: true
  }
});
Enter fullscreen mode Exit fullscreen mode

In our project, our use case allows us to handle both HTTP as well as WebSocket requests thus our Client Root looks a bit different.

import {
  ApolloClient,
  ApolloProvider,
  HttpLink,
  InMemoryCache,
  split,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
import * as React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import config from './config'; // Stores the GraphQL Configuration
import App from './containers/app/App';
import configureStore from './redux/configureStore';
import getToken from './utils/getToken';

const { persistor, store } = configureStore();

// HTTP Link
const httpLink = new HttpLink({
  uri: `${config.grahqlEndpoint}/query`,
});

// Adds Authentication Headers on HTTP as well as was requests
const authLink = setContext((_, { headers }) => {
  const token = getToken();
  return {
    headers: {
      ...headers,
      authorization: token,
    },
  };
});

// WebSocket Link
const wsLink = new WebSocketLink({
  uri: `${config.grahqlEndpointSubscription}/query`,
  options: {
    reconnect: true,
    lazy: true,
  },
});

// Send query request based on the type definition
const link = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  authLink.concat(wsLink),
  authLink.concat(httpLink)
);

// Apollo Client
export const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
});

const ReduxRoot = () => {
  return (
    <ApolloProvider client={client}>
      <Provider store={store}>
        <PersistGate persistor={persistor}>
          <App />
        </PersistGate>
      </Provider>
    </ApolloProvider>
  );
};

export default ReduxRoot;
Enter fullscreen mode Exit fullscreen mode

Using this logic, queries and mutations will use HTTP as normal, and subscriptions will use WebSocket.

Conclusion

You can of-course extend these configurations and play around to create something out of your own. We welcome everyone to comment and let us know about what/how we can improve to achieve more! Every suggestion is appreciated.

Are you an SRE or a Kubernetes enthusiast? Does Chaos Engineering excite you?
Join Our Community On Slack For Detailed Discussion,

To join our slack please follow the following steps!
Step 1: Join the Kubernetes slack
Step 2: Join the #litmus channel on the Kubernetes slack or use this link after joining the Kubernetes slack


Check out the Litmus Chaos GitHub repo and do share your feedback: https://github.com/litmuschaos/litmus
Submit a pull request if you identify any necessary changes.

Don't forget to share these resources with someone who you think might benefit from these. Peace out. ✌🏼

Oldest comments (1)

Collapse
 
ksi9302 profile image
Peter Sugin Kim

You sir saved my day.
I literally did this but then the below line threw error so went around and around making things worse lol

authLink.concat(wsLink),
Enter fullscreen mode Exit fullscreen mode