Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client.
$ yarn add graphql-transport-ws
import { GraphQLSchema, GraphQLObjectType, GraphQLString } 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 };
}
},
},
};
import http from 'http';
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
const server = http.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);
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;
const dispose = client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => {
dispose();
resolve(result);
},
},
);
});
expect(result).toEqual({ hello: 'Hello World!' });
})();
// subscription
(async () => {
const onNext = () => {
/**/
};
await new Promise((resolve, reject) => {
const dispose = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: () => {
dispose();
resolve();
},
},
);
});
expect(onNext).toBeCalledTimes(5); // we say "Hi" in 5 languages
})();
Promisify query execution
import { createClient, SubscribePayload } from '../index';
const client = createClient({
url: 'wss://hey.there/graphql',
});
async function execute<T>(payload: SubscribePayload) {
return new Promise((resolve, reject) => {
let result: T;
const dispose = client.subscribe<T>(payload, {
next: (data) => (result = data),
error: reject,
complete: () => {
dispose();
resolve(result);
},
});
});
}
// use
(async () => {
try {
const result = await execute({
query: '{ hello }',
});
// complete and dispose
// next = result = { data: { hello: 'Hello World!' } }
} catch (err) {
// error
}
})();
Client usage with Relay
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);
Client usage with Apollo
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}`,
};
},
});
Check the docs folder out for TypeDoc generated documentation.
Read about the exact transport intricacies used by the library in the GraphQL over WebSocket Protocol document.
File a bug, contribute with code, or improve documentation? Read up on our guidelines for contributing and drive development with yarn test --watch
away!