Writing a GraphQL WebSocket Subscriber in JavaScript

Rob Blackbourn
2 min readNov 22, 2018

I’ve recently been doing some GraphQL development with a Python server and a React UI. For the client there are two obvious choices:

  • React Relay,
  • Apollo Client.

I spent some time experimenting with both. My application was for a real-time queue monitor, and I spent most of my time trying to defeat the caching layers, as none of the data made sense to cache. I wasn’t sure where the errors we, and there was nothing to help me debug. After much frustration I decided to find a simple non-caching client which supported subscriptions. Unable to find anything I decided to write my own.

The Protocol

Apollo have published the protocol here. The key piece of missing information is the control strings described by the protocol. I found these by examining the graphql-ws-next server implementation.

const GQL = {
CONNECTION_INIT: 'connection_init',
CONNECTION_ACK: 'connection_ack',
CONNECTION_ERROR: 'connection_error',
CONNECTION_KEEP_ALIVE: 'ka',
START: 'start',
STOP: 'stop',
CONNECTION_TERMINATE: 'connection_terminate',
DATA: 'data',
ERROR: 'error',
COMPLETE: 'complete'
}

With the control strings discovered we can start the implementation.

The first step is to create the WebSocket. This requires a url (e.g. “ws://localhost:8080/subscriptions”) and a “protocol”. The server-side implementation shows the magic string is “graphql-ws”.

const webSocket = new WebSocket("ws://localhost:8008/subscriptions", "graphql-ws")

The protocol states that the connection is initialised with a GQL_CONNECTION_INIT being sent from the client with an optional server specific payload as a JSON object.

webSocket.onopen = event => {
this.webSocket.send(JSON.stringify({
type: GQL.CONNECTION_INIT,
payload: this.options
}))
}

Now we need to handle messages returned from the server. The protocol says we either get a GQL_CONNECTION_ACK followed by none or many GQL_CONNECTION_KEEP_ALIVE, or a GQL_CONNECTION_ERROR. Assuming we have passed a callback to accept this data we can do the following.

webSocket.onMessage = event => {
const data = JSON.parse(event.data)
switch (data.type) {
case GQL.CONNECTION_ACK: {
console.log('success')
break
}
case GQL.CONNECTION_ERROR: {
console.error(data.payload)
break
}
case GQL.CONNECTION_KEEP_ALIVE: {
break
}
}

The subscription request is a GQL_START, taking a payload of the query, an object with any variables used by the query, and an optional operationName string. The “id” is sent back so we know which query the data is for.

webSocket.send(JSON.stringify({
type: GQL.START,
id,
payload: { query, variables, operationName }
}))

The data (or errors) returned by this are sent back by the server with the GQL_DATA control message, or GQL_COMPLETE if there is no more data. We can add this to our message handler.

webSocket.onMessage = event => {
const data = JSON.parse(event.data)
switch (data.type) {
case GQL.CONNECTION_ACK: {
console.log('success')
break
}
case GQL.CONNECTION_ERROR: {
console.error(data.payload)
break
}
case GQL.CONNECTION_KEEP_ALIVE: {
break
}
case GQL.DATA: {
console.log(data.id, data.payload.errors, data.payload.data)
break
}
case GQL.COMPLETE: {
console.log('completed', data.id)
break
}
}
}

To unsubscribe we send a GQL_STOP with the id of the original subscription.

webSocket.send(JSON.stringify({
type: GQL.STOP,
id
}))

Finally we can gracefully terminate the WebSocket connection to the server by sending a GQL_CONNECTION_TERMINATE.

webSocket.send(JSON.stringify({
type: GQL.CONNECTION_TERMINATE
}))

Obviously we need to maintain some state to map between subscriptions an ids, and some callbacks to shift the data around.

You can find a full implementation here.

Good luck with your coding.

--

--