1.9.0 • Published 4 years ago
graphql-transport-ws v1.9.0
Getting started
Install
$ yarn add graphql-transport-ws
Create a GraphQL schema
import { buildSchema } from 'graphql';
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Query {
hello: String
}
type Subscription {
greetings: String
}
`);
// The roots provide resolvers for each GraphQL operation
const roots = {
query: {
hello: () => 'Hello World!',
},
subscription: {
greetings: async function* sayHiIn5Languages() {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi };
}
},
},
};
Start the server
import https from 'https';
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
createServer(
{
schema, // from the previous step
roots, // from the previous step
execute,
subscribe,
},
{
server,
path: '/graphql',
},
);
server.listen(443);
Use the client
import { createClient } from 'graphql-transport-ws';
const client = createClient({
url: 'wss://welcomer.com/graphql',
});
// query
(async () => {
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
expect(result).toEqual({ hello: 'Hello World!' });
})();
// subscription
(async () => {
const onNext = () => {
/**/
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5); // we say "Hi" in 5 languages
})();
Recipes
import { createClient, SubscribePayload } from 'graphql-transport-ws';
const client = createClient({
url: 'wss://hey.there/graphql',
});
async function execute<T>(payload: SubscribePayload) {
return new Promise((resolve, reject) => {
let result: T;
client.subscribe<T>(payload, {
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
});
});
}
// use
(async () => {
try {
const result = await execute({
query: '{ hello }',
});
// complete
// next = result = { data: { hello: 'Hello World!' } }
} catch (err) {
// error
}
})();
import { Observable } from 'relay-runtime';
// or
import { Observable } from '@apollo/client';
// or
import { Observable } from 'rxjs';
// or
import Observable from 'zen-observable';
// or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
const client = createClient({
url: 'wss://graphql.loves/observables',
});
function toObservable(operation) {
return new Observable((observer) => client.subscribe(operation, observer));
}
const observable = toObservable({ query: `subscription { ping }` });
const subscription = observable.subscribe({
next: (data) => {
expect(data).toBe({ data: { ping: 'pong' } });
},
});
// ⏱
subscription.unsubscribe();
import {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-transport-ws';
const subscriptionsClient = createClient({
url: 'wss://i.love/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
});
// yes, both fetch AND subscribe handled in one implementation
function fetchOrSubscribe(operation: RequestParameters, variables: Variables) {
return Observable.create((sink) => {
if (!operation.text) {
return sink.error(new Error('Operation text cannot be empty'));
}
return subscriptionsClient.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
{
...sink,
error: (err) => {
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(
new Error(
`Socket closed with event ${err.code}` + err.reason
? `: ${err.reason}` // reason will be available on clean closes
: '',
),
);
} else {
// GraphQLError[]
sink.error(new Error(err.map(({ message }) => message).join(', ')));
}
},
},
);
});
}
export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe);
import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client';
import { createClient, Config, Client } from 'graphql-transport-ws';
class WebSocketLink extends ApolloLink {
private client: Client;
constructor(config: Config) {
super();
this.client = createClient(config);
}
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(operation, {
...sink,
error: (err) => {
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(
new Error(
`Socket closed with event ${err.code}` + err.reason
? `: ${err.reason}` // reason will be available on clean closes
: '',
),
);
} else {
// GraphQLError[]
sink.error(new Error(err.map(({ message }) => message).join(', ')));
}
},
});
});
}
}
const link = new WebSocketLink({
url: 'wss://where.is/graphql',
connectionParams: () => {
const session = getSession();
if (!session) {
return {};
}
return {
Authorization: `Bearer ${session.token}`,
};
},
});
const WebSocket = require('ws'); // yarn add ws
const Crypto = require('crypto');
const { createClient } = require('graphql-transport-ws');
const client = createClient({
url: 'wss://no.browser/graphql',
webSocketImpl: WebSocket,
/**
* Generates a v4 UUID to be used as the ID.
* Reference: https://stackoverflow.com/a/2117523/709884
*/
generateID: () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16),
),
});
// consider other recipes for usage inspiration
import https from 'https';
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { createServer } from 'graphql-transport-ws';
import { execute, subscribe } from 'graphql';
import { schema } from 'my-graphql-schema';
// create express and middleware
const app = express();
app.use('/graphql', graphqlHTTP({ schema }));
// create a http server using express
const server = https.createServer(app);
server.listen(443, () => {
createServer(
{
schema,
execute,
subscribe,
},
{
server,
path: '/graphql', // you can use the same path too, just use the `ws` schema
},
);
});
import https from 'https';
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
createServer(
{
schema,
onConnect: (ctx) => {
console.log('Connect', ctx);
},
onSubscribe: (ctx, msg) => {
console.log('Subscribe', { ctx, msg });
},
onNext: (ctx, msg, args, result) => {
console.debug('Next', { ctx, msg, args, result });
},
onError: (ctx, msg, errors) => {
console.error('Error', { ctx, msg, errors });
},
onComplete: (ctx, msg) => {
console.log('Complete', { ctx, msg });
},
},
{
server,
path: '/graphql',
},
);
server.listen(443);
import https from 'https';
import WebSocket from 'ws';
import url from 'url';
import { execute, subscribe } from 'graphql';
import { createServer, createClient } from 'graphql-transport-ws';
import { schema } from 'my-graphql-schema';
const server = https.createServer(function weServeSocketsOnly(_, res) {
res.writeHead(404);
res.end();
});
/**
* Two websocket servers on different paths:
* - `/wave` sends out waves
* - `/graphql` serves graphql
*/
const waveWS = new WebSocket.Server({ noServer: true });
const graphqlWS = new WebSocket.Server({ noServer: true });
// delegate upgrade requests to relevant destinations
server.on('upgrade', (request, socket, head) => {
const pathname = url.parse(request.url).pathname;
if (pathname === '/wave') {
waveWS.handleUpgrade(request, socket, head, (client) => {
waveWS.emit('connection', client, request);
});
} else if (pathname === '/graphql') {
graphqlWS.handleUpgrade(request, socket, head, (client) => {
graphqlWS.emit('connection', client, request);
});
} else {
socket.destroy();
}
});
// wave on connect
waveWS.on('connection', (socket) => {
socket.send('🌊');
});
// serve graphql
createServer(
{
schema,
execute,
subscribe,
},
graphqlWS,
);
server.listen(443);
import { validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema, roots, getStaticContext } from 'my-graphql';
createServer(
{
context: getStaticContext(),
schema,
roots,
execute,
subscribe,
},
{
server,
path: '/graphql',
},
);
import { parse, validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema, getDynamicContext, myValidationRules } from 'my-graphql';
createServer(
{
execute,
subscribe,
onSubscribe: (ctx, msg) => {
const args = {
schema,
contextValue: getDynamicContext(ctx, msg),
operationName: msg.payload.operationName,
document: parse(msg.payload.operationName),
variableValues: msg.payload.variables,
};
// dont forget to validate when returning custom execution args!
const errors = validate(args.schema, args.document, myValidationRules);
if (errors.length > 0) {
return errors; // return `GraphQLError[]` to send `ErrorMessage` and stop subscription
}
return args;
},
},
{
server,
path: '/graphql',
},
);
// 🛸 server
import { parse, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema } from 'my-graphql-schema';
type QueryID = string;
const queriesStore: Record<QueryID, ExecutionArgs> = {
iWantTheGreetings: {
schema, // you may even provide different schemas in the queries store
document: parse('subscription Greetings { greetings }'),
},
};
createServer(
{
execute,
subscribe,
onSubscribe: (_ctx, msg) => {
// search using `SubscriptionPayload.query` as QueryID
// check the client example below for better understanding
const hit = queriesStore[msg.payload.query];
if (hit) {
return {
...hit,
variableValues: msg.payload.variables, // use the variables from the client
};
}
// if no hit, execute as usual
return {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.operationName),
variableValues: msg.payload.variables,
};
},
},
{
server,
path: '/graphql',
},
);
// 📺 client
import { createClient } from 'graphql-transport-ws';
const client = createClient({
url: 'wss://persisted.graphql/queries',
});
(async () => {
const onNext = () => {
/**/
};
await new Promise((resolve, reject) => {
client.subscribe(
{
query: 'iWantTheGreetings',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
expect(onNext).toBeCalledTimes(5); // greetings in 5 languages
})();
Documentation
Check the docs folder out for TypeDoc generated documentation.
How does it work?
Read about the exact transport intricacies used by the library in the GraphQL over WebSocket Protocol document.
Want to help?
File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch
away!