This package has been deprecated

Author message:

This package has been renamed to graphql-ws.

graphql-transport-ws
TypeScript icon, indicating that this package has built-in type declarations

1.9.0 • Public • Published

GraphQLOverWebSocket

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

Continuous integration graphql-transport-ws


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

Client usage with Promise
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
  }
})();
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 {
  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}`,
    };
  },
});
Client usage in Node
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
Server usage with Express GraphQL
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
    },
  );
});
Server usage with console logging
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);
Server usage on a multi WebSocket server
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);
Server usage with custom static GraphQL arguments
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',
  },
);
Server usage with custom dynamic GraphQL arguments and validation
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 and client usage with persisted queries
// 🛸 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!

Package Sidebar

Install

npm i graphql-transport-ws

Weekly Downloads

453

Version

1.9.0

License

MIT

Unpacked Size

96.8 kB

Total Files

19

Last publish

Collaborators

  • enisdenjo