Reference
GraphQL over WebSockets

GraphQL over WebSockets

GraphQL over WebSockets is supported by the @benzene/ws package. It implements the upcoming GraphQL over WebSocket Protocol.

We would need a spec-compliant client like graphql-ws to connect from the browser.

Create handler function

Use makeHandler from @benzene/ws to create a handler from a Benzene instance.

import { Benzene, makeHandler } from "@benzene/ws";
import schema from "./schema";
 
const GQL = new Benzene({ schema });
 
const graphqlWS = makeHandler(GQL);

WebSocket generic interface

In order to work with any WebSocket server library, Benzene defines a generic interface to be passed into the graphqlWS function.

interface CloseEvent {
  code?: number;
  reason?: string;
}
 
interface MessageEvent {
  data: any;
}
 
interface WebSocket {
  protocol: string;
  send(data: string): void;
  close(code?: number | undefined, reason?: string | undefined): void;
  onclose: (event: CloseEvent) => void;
  onmessage: (event: MessageEvent) => void;
}

This interface is a partial version of the JavaScript WebSockets API since Benzene does not require all the methods in WebSocket nor all the fields in the two events.

Usage in a WebSocket server

The graphqlWS is to be called with a compatible WebSocket instance and it will handle everything (receive/send messages, close connections) automatically. (this is in contrast to @benzene/http).

ws is a library that implements its WebSocket class compliant to the Web API (evidently in its API documentation), so @benzene/ws will work with it naturally.

import { Benzene, makeHandler } from "@benzene/ws";
import WebSocket from "ws";
import schema from "./schema";
 
const GQL = new Benzene({ schema });
 
const graphqlWS = makeHandler(GQL);
 
const wss = new WebSocket.Server({ server });
 
wss.on("connection", (ws) => {
  graphqlWS(ws);
});

Working with non-compatible WebSocket object

In most cases, the WebSocket object we are working with on the server-side will not be similar to the one from the JavaScript Web API. However, we can still work with it by understanding ways that graphqlWS use the object:

  1. Verify whether the protocol is supported via socket.protocol (must be "graphql-transport-ws")

  2. Attach a message listener function to socket.onmessage:

socket.onmessage = (event) => {
  const message = JSON.parse(String(event.data));
  // Handle message object...
};
  1. Use socket.send to send a message to the client:
socket.send(JSON.stringify(data));
  1. Attach a closure listener function to socket.onclose:
socket.onclose = ({ code, reason }) => {
  // do some cleanup
};
  1. Call socket.close when needed (upon fatal error, rogue client, etc):
socket.close(4401, "Some message");

It also expects the socket to call onclose as in step 4.

We can create a custom object with the protocol field and the send and close functions. Passing that object through graphqlWS will augment it with onmessage and onclose. On message or closure, we will then call the attached onmessage and onclose functions from that custom object.

Let's try to work with Deno ws standard library:

async function handleWs(sock: WebSocket): Promise<void> {
  const customSocket = {
    protocol: sock.protocol,
    send: sock.send.bind(sock), // this is similar to browser API, use as it is
    close: sock.close.bind(sock), // this is similar to browser API, use as it is
  };
  graphqlWS(customSocket); // at this point customSocket has onmessage and onclose
  try {
    for await (const ev of sock) {
      // note: Deno actually implement the message and close event
      // similar to  the browser API - use as it is
      if (typeof ev === "string") {
        customSocket.onmessage(ev);
      } else if (isWebSocketCloseEvent(ev)) {
        customSocket.onclose(ev);
      }
    }
  } catch (err) {
    if (!sock.isClosed) {
      await sock.close(1000).catch(console.error);
    }
  }
}

Passing extra

It is possible to pass extra as the second argument to graphqlWS. See The extra argument.