Natural Pleistocene Monsters
Learn about our RFC process, Open RFC meetings & more.Join in the discussion! »

graphql-ws

2.0.0 • Public • Published

GraphQLOverWebSocket

Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client.

Continuous integration graphql-ws


Getting started

Install

$ yarn add graphql-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 ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
 
const server = https.createServer(function weServeSocketsOnly(_, res) {
  res.writeHead(404);
  res.end();
});
 
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
useServer(
  {
    schema, // from the previous step
    roots, // from the previous step
    execute,
    subscribe,
  },
  wsServer,
);
 
server.listen(443);

Use the client

import { createClient } from 'graphql-ws/lib/use/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

🔗 Client usage with Promise
import { createClient, SubscribePayload } from 'graphql-ws/lib/use/ws';
 
const client = createClient({
  url: 'wss://hey.there/graphql',
});
 
async function execute<T>(payload: SubscribePayload) {
  return new Promise<T>((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
  }
})();
🔗 Client usage with AsyncIterator
import { createClient, SubscribePayload } from 'graphql-ws/lib/use/ws';
 
const client = createClient({
  url: 'wss://iterators.ftw/graphql',
});
 
function subscribe<T>(payload: SubscribePayload): AsyncIterableIterator<T> {
  let deferred: {
    resolve: (done: boolean) => void;
    reject: (err: unknown) => void;
  } | null = null;
  const pending: T[] = [];
  let throwMe: unknown = null,
    done = false;
  const dispose = client.subscribe<T>(payload, {
    next: (data) => {
      pending.push(data);
      deferred?.resolve(false);
    },
    error: (err) => {
      throwMe = err;
      deferred?.reject(throwMe);
    },
    complete: () => {
      done = true;
      deferred?.resolve(true);
    },
  });
  return {
    [Symbol.asyncIterator]() {
      return this;
    },
    async next() {
      if (done) return { done: true, value: undefined };
      if (throwMe) throw throwMe;
      if (pending.length) return { value: pending.shift()! };
      return (await new Promise<boolean>(
        (resolve, reject) => (deferred = { resolve, reject }),
      ))
        ? { done: true, value: undefined }
        : { valuepending.shift()! };
    },
    async return() {
      dispose();
      return { done: true, value: undefined };
    },
  };
}
 
(async () => {
  const subscription = subscribe({
    query: 'subscription { greetings }',
  });
  // subscription.return() to dispose
 
  for await (const result of subscription) {
    // next = result = { data: { greetings: 5x } }
  }
  // complete
})();
🔗 Client usage with Observable
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();
🔗 Client usage with Relay
import { GraphQLError } from 'graphql';
import {
  Network,
  Observable,
  RequestParameters,
  Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-ws/lib/use/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 {
            sink.error(
              new Error(
                (err as GraphQLError[])
                  .map(({ message }) => message)
                  .join(''),
              ),
            );
          }
        },
      },
    );
  });
}
 
export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe);
🔗 Client usage with urql
import { createClient, defaultExchanges, subscriptionExchange } from 'urql';
import { createClient as createWSClient } from 'graphql-ws/lib/use/ws';
 
const wsClient = createWSClient({
  url: 'wss://its.urql/graphql',
});
 
const client = createClient({
  url: '/graphql',
  exchanges: [
    ...defaultExchanges,
    subscriptionExchange({
      forwardSubscription(operation) {
        return {
          subscribe: (sink) => {
            const dispose = wsClient.subscribe(operation, sink);
            return {
              unsubscribe: dispose,
            };
          },
        };
      },
    }),
  ],
});
🔗 Client usage with Apollo
import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client';
import { print, GraphQLError } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-ws/lib/use/ws';
 
class WebSocketLink extends ApolloLink {
  private client: Client;
 
  constructor(options: ClientOptions) {
    super();
    this.client = createClient(options);
  }
 
  public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) => {
      return this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          next: sink.next.bind(sink),
          complete: sink.complete.bind(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 {
              sink.error(
                new Error(
                  (err as GraphQLError[])
                    .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}`,
    };
  },
});
🔗 Client usage in browser
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>GraphQL over WebSocket</title>
    <script
      type="text/javascript"
      src="https://unpkg.com/graphql-ws/umd/graphql-ws.min.js"
    ></script> 
  </head>
  <body>
    <script type="text/javascript">
      const client = graphqlWs.createClient({
        url: 'wss://umdfor.the/win/graphql',
      });
 
      // consider other recipes for usage inspiration
    </script> 
  </body>
</html>
🔗 Client usage in Node
const ws = require('ws'); // yarn add ws
const Crypto = require('crypto');
const { createClient } = require('graphql-ws');
 
const client = createClient({
  url: 'wss://no.browser/graphql',
  webSocketImpl: ws,
  /**
   * 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
🔗 Server usage with ws
// minimal version of `import { useServer } from 'graphql-ws/lib/use/ws';`
 
import http from 'http';
import ws from 'ws'; // yarn add ws
import { makeServer, ServerOptions } from 'graphql-ws';
import { execute, subscribe } from 'graphql';
import { schema } from 'my-graphql-schema';
 
// make
const server = makeServer({
  schema,
  execute,
  subscribe,
});
 
// create websocket server
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
// implement
wsServer.on('connection', (socket, request) => {
  const closed = server.opened(
    {
      protocol: socket.protocol,
      send: (data) =>
        new Promise((resolve, reject) => {
          socket.send(data, (err) => (err ? reject(err) : resolve()));
        }),
      close: (code, reason) => socket.close(code, reason),
      onMessage: (cb) =>
        socket.on('message', async (event) => {
          try {
            await cb(event.toString());
          } catch (err) {
            socket.close(1011, err.message);
          }
        }),
    },
    { socket, request },
  );
 
  socket.once('close', () => {
    if (pongWait) clearTimeout(pongWait);
    if (pingInterval) clearInterval(pingInterval);
    closed();
  });
});
🔗 ws server usage with Express GraphQL
import https from 'https';
import ws from 'ws'; // yarn add ws
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { useServer } from 'graphql-ws/lib/use/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);
 
// create websocket server
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
server.listen(443, () => {
  useServer(
    {
      schema,
      execute,
      subscribe,
    },
    wsServer,
  );
});
🔗 ws server usage with Apollo Server Express
import https from 'https';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import { schema } from 'my-graphql-schema';
 
// create express
const app = express();
 
// create apollo server
const apolloServer = new ApolloServer({ schema });
 
// apply middleware
apolloServer.applyMiddleware({ app });
 
// create a http server using express
const server = https.createServer(app);
 
// create websocket server
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
server.listen(443, () => {
  useServer(
    {
      schema,
      execute,
      subscribe,
    },
    wsServer,
  );
});
🔗 ws server usage with console logging
import https from 'https';
import { execute, subscribe } from 'graphql';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from 'my-graphql-schema';
 
const server = https.createServer(function weServeSocketsOnly(_, res) {
  res.writeHead(404);
  res.end();
});
 
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
useServer(
  {
    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 });
    },
  },
  wsServer,
);
 
server.listen(443);
🔗 ws server usage on a multi WebSocket server
import https from 'https';
import ws from 'ws'; // yarn add ws
import url from 'url';
import { execute, subscribe } from 'graphql';
import { useServer, createClient } from 'graphql-ws/lib/use/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 ws.Server({ noServer: true });
const graphqlWS = new ws.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
useServer(
  {
    schema,
    execute,
    subscribe,
  },
  graphqlWS,
);
 
server.listen(443);
🔗 ws server usage with custom context value
import { validate, execute, subscribe } from 'graphql';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, roots, getDynamicContext } from 'my-graphql';
 
useServer(
  {
    context: (ctx, msg, args) => {
      return getDynamicContext(ctx, msg, args);
    }, // or static context by supplying the value direcly
    schema,
    roots,
    execute,
    subscribe,
  },
  wsServer,
);
🔗 ws server usage with custom execution arguments and validation
import { parse, validate, execute, subscribe } from 'graphql';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema, myValidationRules } from 'my-graphql';
 
useServer(
  {
    execute,
    subscribe,
    onSubscribe: (ctx, msg) => {
      const args = {
        schema,
        operationName: msg.payload.operationName,
        document: parse(msg.payload.query),
        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;
    },
  },
  wsServer,
);
🔗 ws server and client usage with persisted queries
// 🛸 server
 
import { parse, execute, subscribe } from 'graphql';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from 'my-graphql-schema';
 
// a unique GraphQL execution ID used for representing
// a query in the persisted queries store. when subscribing
// you should use the `SubscriptionPayload.query` to transmit the id
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 }'),
  },
};
 
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});
 
useServer(
  {
    execute,
    subscribe,
    onSubscribe: (_ctx, msg) => {
      const query = queriesStore[msg.payload.query];
      if (!query) {
        // for extra security you only allow the queries from the store
        throw new Error('404: Query Not Found');
      }
      return {
        ...query,
        variableValues: msg.payload.variables, // use the variables from the client
      };
    },
  },
  wsServer,
);
// 📺 client
 
import { createClient } from 'graphql-ws/lib/use/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!

Install

npm i graphql-ws

DownloadsWeekly Downloads

2,269

Version

2.0.0

License

MIT

Unpacked Size

154 kB

Total Files

24

Last publish

Collaborators

  • avatar