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:
-
Verify whether the protocol is supported via
socket.protocol
(must be"graphql-transport-ws"
) -
Attach a message listener function to
socket.onmessage
:
socket.onmessage = (event) => {
const message = JSON.parse(String(event.data));
// Handle message object...
};
- Use
socket.send
to send a message to the client:
socket.send(JSON.stringify(data));
- Attach a closure listener function to
socket.onclose
:
socket.onclose = ({ code, reason }) => {
// do some cleanup
};
- 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.