1.9.0 • Published 4 years ago

graphql-transport-ws v1.9.0

Weekly downloads
119
License
MIT
Repository
github
Last release
4 years ago

GraphQLOverWebSocket

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

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!

1.9.0

4 years ago

1.8.2

4 years ago

1.8.1

4 years ago

1.8.0

4 years ago

1.7.0

4 years ago

1.6.0

4 years ago

1.5.0

4 years ago

1.4.2

4 years ago

1.4.1

4 years ago

1.4.0

4 years ago

1.3.0

4 years ago

1.2.0

4 years ago

0.8.3

7 years ago