Skip to content

Commit

Permalink
Backport PR #9369 to main branch for GraphQLWsLink (#9453)
Browse files Browse the repository at this point in the history
New `GraphQLWsLink` and `@apollo/client/link/subscriptions` sub-package
entry point for `graphql-ws` subscriptions library.
  • Loading branch information
benjamn authored Feb 24, 2022
1 parent a8da797 commit ce90c47
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 46 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@

## Apollo Client 3.5.10 (unreleased)

### Improvements

- Add `GraphQLWsLink` in `@apollo/client/link/subscriptions`. This link is similar to the existing `WebSocketLink` in `@apollo/client/link/ws`, but uses the newer [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) package and protocol instead of the older `subscriptions-transport-ws` implementation. <br/>
[@glasser](https://github.com/glasser) in [#9369](https://github.com/apollographql/apollo-client/pull/9369)

> Note from [@benjamn](https://github.com/benjamn): since `GraphQLWsLink` is new functionality, we would normally wait for the next minor version (v3.6), but we were asked to expedite this release. These changes are strictly additive/opt-in/backwards-compatible, so shipping them in a patch release (3.5.10) seems safe, if unusual.
## Apollo Client 3.5.9 (2022-02-15)

### Improvements
Expand Down
1 change: 1 addition & 0 deletions config/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const entryPoints = [
{ dirs: ['link', 'persisted-queries'] },
{ dirs: ['link', 'retry'] },
{ dirs: ['link', 'schema'] },
{ dirs: ['link', 'subscriptions'] },
{ dirs: ['link', 'utils'] },
{ dirs: ['link', 'ws'] },
{ dirs: ['react'] },
Expand Down
1 change: 1 addition & 0 deletions docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ module.exports = {
'api/link/apollo-link-rest',
'api/link/apollo-link-retry',
'api/link/apollo-link-schema',
'api/link/apollo-link-subscriptions',
'api/link/apollo-link-ws',
'api/link/community-links'
],
Expand Down
39 changes: 39 additions & 0 deletions docs/source/api/link/apollo-link-subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Subscriptions Link
sidebar_title: Subscriptions (newer protocol)
description: Execute subscriptions (or other GraphQL operations) over WebSocket with the `graphql-ws` library
api_reference: true
---

> We recommend reading [Apollo Link overview](./introduction/) before learning about individual links.
The `GraphQLWsLink` is a [terminating link](./introduction/#the-terminating-link) that's used most commonly with GraphQL [subscriptions](../../data/subscriptions/) (which usually communicate over WebSocket), although you can send queries and mutations over WebSocket as well.

`GraphQLWsLink` requires the [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) library. Install it in your project like so:

```shell
npm install graphql-ws
```

> **Note**: This link works with the newer `graphql-ws` library. If your server uses the older `subscriptions-transport-ws`, you should use the [`WebSocketLink` link from `@apollo/client/link/ws](./apollo-link-ws) instead.
## Constructor

```js
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";

const link = new GraphQLWsLink(createClient({
url: "ws://localhost:3000/subscriptions",
}));
```

### Options

The `GraphQLWsLink` constructor takes a single argument, which is a `Client` returned from the `graphql-ws` `createClient` function.

The `createClient` function can take many options; full details can be found in [the `graphql-ws` docs for `ClientOptions`](https://github.com/enisdenjo/graphql-ws/blob/master/docs/interfaces/client.ClientOptions.md). The one required option is `url`, which is the URL (typically starting with `ws://` or `wss://`, which are the equivalents of `http://` and `https://` respectively) to your WebSocket server. (Note that this differs from the [older link's URL option](./apollo-link-ws) which is called `uri` rather than `url`.)

## Usage

See [Subscriptions](../../data/subscriptions/).
23 changes: 14 additions & 9 deletions docs/source/api/link/apollo-link-ws.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: WebSocket Link
sidebar_title: WebSocket
description: Execute subscriptions (or other GraphQL operations) over WebSocket
sidebar_title: WebSocket (older protocol)
description: Execute subscriptions (or other GraphQL operations) over WebSocket with the `subscriptions-transport-ws` library
api_reference: true
---

Expand All @@ -15,22 +15,27 @@ The `WebSocketLink` is a [terminating link](./introduction/#the-terminating-link
npm install subscriptions-transport-ws
```

> **Note**: The `subscriptions-transport-ws` library is not actively maintained. We recommend the use of the `graphql-ws` library instead. These libraries layer different protocols on top of WebSockets, so you do need to ensure you are using the same library in your server and any clients that you support. To use `graphql-ws` from Apollo Client, use the [`GraphQLWsLink` link from `@apollo/client/link/subscriptions](./apollo-link-subscriptions) instead.
## Constructor

```js
import { WebSocketLink } from "@apollo/client/link/ws";

const link = new WebSocketLink({
uri: "ws://localhost:3000/subscriptions",
options: {
reconnect: true
}
import { SubscriptionClient } from "subscriptions-transport-ws";

const link = new WebSocketLink(
new SubscriptionClient({
uri: "ws://localhost:3000/subscriptions",
options: {
reconnect: true,
},
}),
});
```

### Options

The `WebSocketLink` constructor takes an options object with the following fields:
The `WebSocketLink` constructor takes either a `SubscriptionClient` object or an options object with the following fields. (These options are passed directly to the `SubscriptionClient` constructor.)

<table class="field-table">
<thead>
Expand Down
2 changes: 1 addition & 1 deletion docs/source/api/react/hoc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ data.fetchMore({

### `data.subscribeToMore(options)`

This function will set up a subscription, triggering updates whenever the server sends a subscription publication. This requires subscriptions to be set up on the server to properly work. Check out the [subscriptions guide](../../data/subscriptions/) and the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) and [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) for more information on getting this set up.
This function will set up a subscription, triggering updates whenever the server sends a subscription publication. This requires subscriptions to be set up on the server to properly work. Check out the [subscriptions guide](../../data/subscriptions/) for more information on getting this set up.

This function returns an `unsubscribe` function handler which can be used to unsubscribe later.

Expand Down
105 changes: 70 additions & 35 deletions docs/source/data/subscriptions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ You _should_ use subscriptions for the following:

* **Low-latency, real-time updates**. For example, a chat application's client wants to receive new messages as soon as they're available.

## Choice of subscription protocol

The GraphQL spec does not define a specific way to send subscription requests. The first popular JavaScript library to implement subscriptions over WebSocket is called `subscriptions-transport-ws`. This library is no longer actively maintained; its successor is a library called `graphql-ws`. The two packages _do not use the same protocol_, so you need to make sure that your server and clients all use the same library.

Apollo Client supports both `graphql-ws` and `subscriptions-transport-ws`. We recommend you use the newer library `graphql-ws` and this page shows how to use it. If you need to use `subscriptions-transport-ws` because your server still uses that protocol, the differences are described [at the bottom of this page](#the-older-subscriptions-transport-ws-library).

> **Note**: When looking at the source code of an implementation to determine which protocol it supports, you will find that the libraries uses different strings as the "WebSocket subprotocol". Confusingly, `subscriptions-transport-ws` uses the `graphql-ws` subprotocol and `graphql-ws` uses the `graphql-transport-ws` subprotocol! In these docs, when we say "`graphql-ws`" we are referring to the _library_ `graphql-ws`, not the subprotocol `graphql-ws`, which is the other project.
## Defining a subscription

You define a subscription on both the server side and the client side, just like you do for queries and mutations.
Expand Down Expand Up @@ -70,58 +78,54 @@ Whenever your GraphQL server _does_ push data to a subscribing client, that data

## Setting up the transport

Because subscriptions usually maintain a persistent connection, they shouldn't use the default HTTP transport that Apollo Client uses for queries and mutations. Instead, Apollo Client subscriptions most commonly communicate over WebSocket, via the community-maintained [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws) library.
Because subscriptions usually maintain a persistent connection, they shouldn't use the default HTTP transport that Apollo Client uses for queries and mutations. Instead, Apollo Client subscriptions most commonly communicate over WebSocket, via the [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) library. (As mentioned [above](#choice-of-subscription-protocol), some servers use an older library called `subscriptions-transport-ws`; see [below](#the-older-subscriptions-transport-ws-library) for the changes necessary to use that library with Apollo Client.)

### 1. Install required libraries

[Apollo Link](../api/link/introduction/) is a library that helps you customize Apollo Client's network communication. You can use it to define a **link chain** that modifies your operations and routes them to the appropriate destination.

To execute subscriptions over WebSocket, you can add a `WebSocketLink` to your link chain. This link requires the `subscriptions-transport-ws` library. Install it like so:
To execute subscriptions over WebSocket, you can add a `GraphQLWsLink` to your link chain. This link requires the `graphql-ws` library. Install it like so:

```bash
npm install subscriptions-transport-ws
npm install graphql-ws
```

### 2. Initialize a `WebSocketLink`
### 2. Initialize a `GraphQLWsLink`

Import and initialize a `WebSocketLink` object in the same project file where you initialize `ApolloClient`:
Import and initialize a `GraphQLWsLink` object in the same project file where you initialize `ApolloClient`:

```js:title=index.js
import { WebSocketLink } from '@apollo/client/link/ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true
}
});
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
}));
```

Replace the value of the `uri` option with your GraphQL server's subscription-specific WebSocket endpoint. If you're using Apollo Server, see [Setting a subscription endpoint](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#setting-a-subscription-endpoint).
Replace the value of the `url` option with your GraphQL server's subscription-specific WebSocket endpoint. If you're using Apollo Server, see [Setting a subscription endpoint](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#setting-a-subscription-endpoint).

### 3. Split communication by operation (recommended)

Although Apollo Client _can_ use your `WebSocketLink` to execute all operation types, in most cases it should continue using HTTP for queries and mutations. This is because queries and mutations don't require a stateful or long-lasting connection, making HTTP more efficient and scalable if a WebSocket connection isn't already present.
Although Apollo Client _can_ use your `GraphQLWsLink` to execute all operation types, in most cases it should continue using HTTP for queries and mutations. This is because queries and mutations don't require a stateful or long-lasting connection, making HTTP more efficient and scalable if a WebSocket connection isn't already present.

To support this, the `@apollo/client` library provides a `split` function that lets you use one of two different `Link`s, according to the result of a boolean check.

The following example expands on the previous one by initializing both a `WebSocketLink` _and_ an `HttpLink`. It then uses the `split` function to combine those two `Link`s into a _single_ `Link` that uses one or the other according to the type of operation being executed.
The following example expands on the previous one by initializing both a `GraphQLWsLink` _and_ an `HttpLink`. It then uses the `split` function to combine those two `Link`s into a _single_ `Link` that uses one or the other according to the type of operation being executed.

```js:title=index.js
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});

const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true
}
});
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
}));

// The split function takes three parameters:
//
Expand Down Expand Up @@ -162,24 +166,21 @@ const client = new ApolloClient({
### 5. Authenticate over WebSocket (optional)

It is often necessary to authenticate a client before allowing it to receive subscription results. To do this, you can provide a `connectionParams` option to the `WebSocketLink` constructor, like so:
It is often necessary to authenticate a client before allowing it to receive subscription results. To do this, you can provide a `connectionParams` option to the `GraphQLWsLink` constructor, like so:

```js{7-9}
import { WebSocketLink } from '@apollo/client/link/ws';
```js{6-8}
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true,
connectionParams: {
authToken: user.authToken,
},
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authToken: user.authToken,
},
});
}));
```

Your `WebSocketLink` passes the `connectionParams` object to your server whenever it connects. If your server has a [SubscriptionsServer](https://www.apollographql.com/docs/graphql-subscriptions/authentication) object that's listening for WebSocket connections, it receives the `connectionParams` object and can use it to perform authentication, along with any other connection-related tasks.

Your `GraphQLWsLink` passes the `connectionParams` object to your server whenever it connects. Your server receives the `connectionParams` object and can use it to perform authentication, along with any other connection-related tasks.

## Executing a subscription

Expand Down Expand Up @@ -312,3 +313,37 @@ The `useSubscription` Hook accepts the following options:
After being called, the `useSubscription` Hook returns a result object with the following properties:

<SubscriptionResult />

## The older `subscriptions-transport-ws` library

If your server uses `subscriptions-transport-ws` instead of the newer `graphql-ws` library, you need to make a few changes to how you set up your link.

Instead of `npm install graphql-ws`, you `npm install subscriptions-transport-ws`.

Instead of `import { createClient } from 'graphql-ws'`, you `import { SubscriptionClient } from 'subscriptions-transport-ws'`.

Instead of `import { GraphQLWsLink } from '@apollo/client/link/subscriptions'`, you `import { WebSocketLink } from '@apollo/client/link/ws`.

The options passed to `new SubscriptionClient` are slightly different from those passed to `createClient`. The subscriptions URL is specified in an `uri` option instead of an `url` option. The `connectionParams` option is nested under an options object called `options` instead of being at the top level. (You may also pass the `new SubscriptionClient` constructor arguments directly to `new WebSocketLink`.) See [the `subscriptions-transport-ws` README](https://www.npmjs.com/package/subscriptions-transport-ws) for complete `SubscriptionClient` API docs.

Once you've created your `wsLink`, everything else in this document still applies: `useSubscription`, `subscribeToMore`, and split links work exactly the same way for both implementations.

The following is what typical `WebSocketLink` initialization looks like:

```js
import { WebSocketLink } from "@apollo/client/link/ws";
import { SubscriptionClient } from "subscriptions-transport-ws";

const wsLink = new WebSocketLink(
new SubscriptionClient({
uri: "ws://localhost:4000/subscriptions",
options: {
connectionParams: {
authToken: user.authToken,
},
},
}),
});
```

More details on `WebSocketLink`'s API can be found in [its API docs](../api/link/apollo-link-ws).
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@
},
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
"graphql-ws": "^5.5.5",
"react": "^16.8.0 || ^17.0.0",
"subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
},
"peerDependenciesMeta": {
"graphql-ws": {
"optional": true
},
"react": {
"optional": true
},
Expand Down Expand Up @@ -110,6 +114,7 @@
"fetch-mock": "9.11.0",
"glob": "7.2.0",
"graphql": "16.0.1",
"graphql-ws": "5.6.2",
"jest": "27.5.1",
"jest-fetch-mock": "3.0.3",
"jest-junit": "13.0.0",
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ Array [
]
`;

exports[`exports of public entry points @apollo/client/link/subscriptions 1`] = `
Array [
"GraphQLWsLink",
]
`;

exports[`exports of public entry points @apollo/client/link/utils 1`] = `
Array [
"createOperation",
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as linkHTTP from "../link/http";
import * as linkPersistedQueries from "../link/persisted-queries";
import * as linkRetry from "../link/retry";
import * as linkSchema from "../link/schema";
import * as linkSubscriptions from "../link/subscriptions";
import * as linkUtils from "../link/utils";
import * as linkWS from "../link/ws";
import * as react from "../react";
Expand Down Expand Up @@ -52,6 +53,7 @@ describe('exports of public entry points', () => {
check("@apollo/client/link/persisted-queries", linkPersistedQueries);
check("@apollo/client/link/retry", linkRetry);
check("@apollo/client/link/schema", linkSchema);
check("@apollo/client/link/subscriptions", linkSubscriptions);
check("@apollo/client/link/utils", linkUtils);
check("@apollo/client/link/ws", linkWS);
check("@apollo/client/react", react);
Expand Down
Loading

0 comments on commit ce90c47

Please sign in to comment.