Jingshao

Graphql Subscription with Angular and Rxjs

In this article, I want to show you how easy it is to use only Angular and its build in Rxjs to do Graphql subscription. (There are existing graphql clients such as Apollo Client, but we show you how simple it it to roll your own.)

Graphql has a subscription type besides query and mutation. Most implementations use websocket to implement subscription type and send data from server to client.

This is a very nice feature if the client needs to track server’s data in realtime. You can periodically query update from the server, but this is not efficient. GQL subscription solve this problem nicely: you subscribe only once, then just wait for the server to send you updates. This is exactly where the stream model of Rxjs fits in perfectly.

GraphQL Server

We focus on the client implementation, so I just use a python GQL server called Ariadne and copy their example without detail explanations (pip install ariadne uvicorn). An Apollo server should work the same.

All the server do is to increment the counter every second, five times total.

import asyncio
from ariadne import SubscriptionType, make_executable_schema
from ariadne.asgi import GraphQL
type_def = """
    type Query {
        _unused: Boolean
    }
    type Subscription {
        counter: Int!
    }
"""
subscription = SubscriptionType()

@subscription.source("counter")
async def counter_generator(obj, info):
    for i in range(5):
        await asyncio.sleep(1)
        yield i

@subscription.field("counter")
def counter_resolver(count, info):
    return count + 1

schema = make_executable_schema(type_def, subscription)
app = GraphQL(schema, debug=True)

Save it as server.py then start the server by running:

uvicorn server:app

Now launch a browser and go to http://localhost:8000, you should be presented with the GQL playground interface.

In the playground you can try subscription { counter } manually and see the output to make sure the server is working.

Angular

Create a new angular app by using angular-cli:

ng new gql-test

Clean up src/app/app.component.html to have just a counter and a button:

<div style="text-align:center">
  <h1>Counter: {{counter}}</h1>
  <button (click)="sub()">GQL Subscribe</button>
</div>

What we want to do is to click the GQL Subscribe button, and see the Counter increasing every second.

WebSocket in the Component

The subscription feature are implemented in WebSocket. WebSocket establishes one tcp connection to the server and does not tear it down as regular http does. The server can send data over the tcp connection to the client at any time.

WebSocket is defined in rxjs/webSocket. Add this line to src/app/app.component.ts:

import { webSocket, WebSocketSubject } from 'rxjs/webSocket'

Add a member gqlSocket: WebSocketSubject<any> and start the web socket on init:

export class AppComponent implements OnInit {

  gqlSocket: WebSocketSubject<any>

  ngOnInit() {
    this.gqlSocket = webSocket({
      url: 'ws://localhost:8000',
      protocol: 'graphql-ws'
    })
  }
}

Now we have a websocket, and we connect it to our GQL server when the component is initialized.

Sending query subscription and subscript to response

At this step, you should be able to do and ng serve and browse to http://localhost:4200. From the browser’s developer tool, you will see the web socket has be established.

Figure 1

All we need to do now is to send command and get results through this socket. But what is the format?

Simple! Just go to the network tab in the developer tool on the Graphql playground page, and check the communication.

Figure 2

As you can see, the client first send a connection_init message, then a start message with a query string in payload field, then the server starts to send data back, until it finished with a complete message.

Nice! Now all we need to do is to send the same message and wait for the results!

Send message and parse results

You call next() on the websocket to send a message to the server, and subscribe on the websocket to get a response every time the server sends a message back. That is the sweet spot of Rxjs!

ngOnInit() {
  this.gqlSocket = webSocket({
    url: 'ws://localhost:8000',
    protocol: 'graphql-ws'
  })
  this.gqlSocket.subscribe(response => {
    console.log(response)
  })
}
sub() {
  this.gqlSocket.next({ "type": "connection_init", "payload": {} })
  this.gqlSocket.next({
    "id": "1",
    "type": "start",
    "payload": { 
      "variables": {},
      "extensions": {},
      "operationName": null,
      "query": "subscription {counter\n}\n"
    }
  })
}

In above code, when we click the button, the component will send two messages to the GQL server: connection_init and start. The component will also subscribe to any responses from the server by subscribing to the gqlSocket observable.

Now, let’s go to http://localhost:4200 and click the button, we will see the response being printed out in the console! Our very simple subscription client is working!

Complete code

Do some experiments in the next function and we can figure out the following about the protocol used in GQL subscription:

  1. The connection_init message is not necessary.
  2. Whatever you put in the id field, will be reflected back in the response.
  3. {} or null fields can be omitted

For those who want to find the details, here is the protocol specification from Apollo Server.

For our own small example, the complete component .ts file is here:

import { Component, OnInit } from '@angular/core'
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  // GQL subscription websoket spec:
  // https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md

  counter = 0
  gqlSocket: WebSocketSubject<any>

  ngOnInit() {
    this.gqlSocket = webSocket({
      url: 'ws://localhost:8000',
      protocol: 'graphql-ws'
    })
    this.gqlSocket.subscribe(response => {
      switch (response.type) {
        case 'connection_ack':
          console.log('connection ack')
          break
        case 'complete':
          console.log('sub completed')
          break
        case 'data':
          this.counter = response.payload.data.counter
          break
        default:
          // not parsed
          console.log(response)
      }
    })
  }
  sub() {
    let id = 'random id'
    let query = `subscription {
     counter
    }`
    this.gqlSocket.next({
      id: id,
      type: 'start',
      payload: {
        query: query
      }
    })
  }
}

Conclusion

GQL subscription is a natural fit for Rxjs’s observer/subscriber stream model. Handling GQL subscription in Angular is quite straight forward and easy.


Share

comments powered by Disqus