Skip to content

Commit 3a09b2e

Browse files
authored
Add support for websocket endpoint with graphql-ws (#1326)
1 parent 2077b6a commit 3a09b2e

File tree

7 files changed

+150
-16
lines changed

7 files changed

+150
-16
lines changed

packages/graphql-playground-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"escape-html": "^1.0.3",
125125
"graphiql": "^0.17.5",
126126
"graphql": "^15.3.0",
127+
"graphql-ws": "^4.5.0",
127128
"immutable": "^4.0.0-rc.9",
128129
"isomorphic-fetch": "^2.2.1",
129130
"js-yaml": "^3.10.0",

packages/graphql-playground-react/src/components/EndpointPopup.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import Popup from './Popup'
44
import { throttle } from 'lodash'
55
import { Button } from './Button'
66
import { styled, css } from '../styled'
7+
import { createClient } from 'graphql-ws';
8+
79

810
// @ts-ignore
911
import imageSource from '../assets/logo.png'
@@ -20,7 +22,37 @@ export interface State {
2022

2123
export default class EndpointPopup extends React.Component<Props, State> {
2224
checkEndpoint = throttle(() => {
23-
if (this.state.endpoint.match(/^https?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?.*$/)) {
25+
if (this.state.endpoint.match(/^(https?|wss?):\/\/\w+(\.\w+)*(:[0-9]+)?\/?.*$/)) {
26+
if (this.state.endpoint.match(/^(wss?)/)) {
27+
const client = createClient({
28+
url: this.state.endpoint,
29+
retryAttempts:0
30+
});
31+
const unsubscribe = client.subscribe(
32+
{
33+
query: `{
34+
__schema {
35+
queryType {
36+
kind
37+
}
38+
}
39+
}`,
40+
},
41+
{
42+
next: () => {
43+
this.setState({ valid: true })
44+
unsubscribe()
45+
},
46+
error: () => {
47+
this.setState({ valid: false });
48+
unsubscribe()
49+
},
50+
complete: () => {},
51+
},
52+
);
53+
return;
54+
}
55+
2456
fetch(this.state.endpoint, {
2557
method: 'post',
2658
headers: {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ApolloLink, Operation, FetchResult, Observable } from 'apollo-link';
2+
import { print, GraphQLError } from 'graphql';
3+
import { Client } from 'graphql-ws';
4+
5+
export class WebSocketLink extends ApolloLink {
6+
7+
constructor(private client: Client) {
8+
super();
9+
}
10+
11+
public request(operation: Operation): Observable<FetchResult> {
12+
return new Observable((sink) => {
13+
return this.client.subscribe<FetchResult>(
14+
{ ...operation, query: print(operation.query) },
15+
{
16+
next: sink.next.bind(sink),
17+
complete: sink.complete.bind(sink),
18+
error: (err) => {
19+
if (err instanceof Error) {
20+
sink.error(err);
21+
} else if (err instanceof CloseEvent) {
22+
sink.error(
23+
new Error(
24+
`Socket closed with event ${err.code}` + err.reason
25+
? `: ${err.reason}` // reason will be available on clean closes
26+
: '',
27+
),
28+
);
29+
} else {
30+
sink.error(
31+
new Error(
32+
(err as GraphQLError[])
33+
.map(({ message }) => message)
34+
.join(', '),
35+
),
36+
);
37+
}
38+
},
39+
},
40+
);
41+
});
42+
}
43+
}

packages/graphql-playground-react/src/state/sessions/fetchingSagas.ts

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { ApolloLink, execute } from 'apollo-link'
22
import { parseHeaders } from '../../components/Playground/util/parseHeaders'
3-
import { SubscriptionClient } from 'subscriptions-transport-ws'
43
import { HttpLink } from 'apollo-link-http'
5-
import { WebSocketLink } from 'apollo-link-ws'
64
import { isSubscription } from '../../components/Playground/util/hasSubscription'
75
import {
86
takeLatest,
@@ -38,6 +36,10 @@ import { Session, ResponseRecord } from './reducers'
3836
import { addHistoryItem } from '../history/actions'
3937
import { safely } from '../../utils'
4038
import { set } from 'immutable'
39+
import { SubscriptionClient as SubscriptionClientSTWS } from 'subscriptions-transport-ws'
40+
import { WebSocketLink as WebSocketLinkALW } from 'apollo-link-ws'
41+
import { createClient as createSubscriptionClient, Client as SubscriptionClientGWS } from 'graphql-ws'
42+
import { WebSocketLink as WebSocketLinkGW } from './WebSocketLink'
4143

4244
// tslint:disable
4345
let subscriptionEndpoint
@@ -50,18 +52,22 @@ export interface LinkCreatorProps {
5052
endpoint: string
5153
headers?: Headers
5254
credentials?: string
55+
subscriptionTransport?: string
5356
}
5457

5558
export interface Headers {
5659
[key: string]: string | number | null
5760
}
5861

62+
const isWSEndpoint = (endpoint: string): boolean => !!endpoint.match(/wss?/);
63+
5964
export const defaultLinkCreator = (
6065
session: LinkCreatorProps,
6166
subscriptionEndpoint?: string,
62-
): { link: ApolloLink; subscriptionClient?: SubscriptionClient } => {
67+
): { link: ApolloLink; subscriptionClient?: SubscriptionClientGWS | SubscriptionClientSTWS } => {
68+
6369
let connectionParams = {}
64-
const { headers, credentials } = session
70+
const { headers, credentials, subscriptionTransport } = session
6571

6672
if (headers) {
6773
connectionParams = { ...headers }
@@ -73,21 +79,53 @@ export const defaultLinkCreator = (
7379
credentials,
7480
})
7581

76-
if (!subscriptionEndpoint) {
77-
return { link: httpLink }
82+
// ws endpoint => graphql-ws default link
83+
if (isWSEndpoint(session.endpoint)) {
84+
const subscriptionClient = createSubscriptionClient({
85+
retryAttempts: 1000,
86+
retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)),
87+
lazy: true,
88+
connectionParams,
89+
url: session.endpoint,
90+
})
91+
92+
return {
93+
link: new WebSocketLinkGW(subscriptionClient),
94+
subscriptionClient,
95+
}
96+
}
97+
98+
// http endpoint & graphql-ws => default link = http + graphql-ws subscriptions
99+
if (subscriptionTransport === 'graphql-ws') {
100+
const subscriptionClient = createSubscriptionClient({
101+
retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)),
102+
lazy: true,
103+
connectionParams,
104+
url: subscriptionEndpoint || session.endpoint.replace('http', 'ws'),
105+
})
106+
107+
return {
108+
subscriptionClient,
109+
link: new WebSocketLinkGW(subscriptionClient)
110+
}
78111
}
79112

80-
const subscriptionClient = new SubscriptionClient(subscriptionEndpoint, {
81-
timeout: 20000,
82-
lazy: true,
83-
connectionParams,
84-
})
113+
// http endpoint => default link = http + subscriptions-transport-ws subscriptions
114+
const subscriptionClient = new SubscriptionClientSTWS(
115+
subscriptionEndpoint || session.endpoint.replace('http', 'ws'),
116+
{
117+
timeout: 20000,
118+
lazy: true,
119+
connectionParams,
120+
}
121+
)
122+
123+
const webSocketLink = new WebSocketLinkALW(subscriptionClient);
85124

86-
const webSocketLink = new WebSocketLink(subscriptionClient)
87125
return {
88126
link: ApolloLink.split(
89127
operation => isSubscription(operation),
90-
webSocketLink as any,
128+
webSocketLink,
91129
httpLink,
92130
),
93131
subscriptionClient,
@@ -107,6 +145,12 @@ export function setLinkCreator(newLinkCreator) {
107145

108146
const subscriptions = {}
109147

148+
const isSubscriptionClientSTWS = (
149+
client: SubscriptionClientGWS | SubscriptionClientSTWS
150+
): client is SubscriptionClientSTWS => {
151+
return !!(client as SubscriptionClientSTWS).onDisconnected
152+
}
153+
110154
function* runQuerySaga(action) {
111155
// run the query
112156
const { operationName } = action.payload
@@ -127,12 +171,14 @@ function* runQuerySaga(action) {
127171
if (session.tracingSupported && session.responseTracingOpen) {
128172
headers = set(headers, 'X-Apollo-Tracing', '1')
129173
}
174+
130175
const lol = {
131176
endpoint: session.endpoint,
132177
headers: {
133178
...settings['request.globalHeaders'],
134179
...headers,
135180
},
181+
subscriptionTransport: settings['subscriptions.protocol'],
136182
credentials: settings['request.credentials'],
137183
}
138184

@@ -143,15 +189,20 @@ function* runQuerySaga(action) {
143189
const channel = eventChannel(emitter => {
144190
let closed = false
145191
if (subscriptionClient && operationIsSubscription) {
146-
subscriptionClient.onDisconnected(() => {
192+
const onDisconnect = () => {
147193
closed = true
148194
emitter({
149195
error: new Error(
150196
`Could not connect to websocket endpoint ${subscriptionEndpoint}. Please check if the endpoint url is correct.`,
151197
),
152198
})
153199
emitter(END)
154-
})
200+
}
201+
if (isSubscriptionClientSTWS(subscriptionClient)) {
202+
subscriptionClient.onDisconnected(onDisconnect)
203+
} else {
204+
subscriptionClient.on('closed', onDisconnect)
205+
}
155206
}
156207
const subscription = execute(link, operation).subscribe({
157208
next: function(value) {

packages/graphql-playground-react/src/state/workspace/reducers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const defaultSettings: ISettings = {
5858
'schema.polling.interval': 2000,
5959
'tracing.hideTracingResponse': true,
6060
'tracing.tracingSupported': true,
61+
'subscriptions.protocol': 'subscription-transport-ws',
6162
}
6263

6364
// tslint:disable-next-line:max-classes-per-file

packages/graphql-playground-react/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export interface ISettings {
3434
['schema.polling.interval']: number
3535
['tracing.hideTracingResponse']: boolean
3636
['tracing.tracingSupported']: boolean
37+
['subscriptions.protocol']: 'subscription-transport-ws' | 'graphql-ws'
3738
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8496,6 +8496,11 @@ graphql-request@^1.4.0, graphql-request@^1.5.0:
84968496
dependencies:
84978497
cross-fetch "2.2.2"
84988498

8499+
graphql-ws@^4.5.0:
8500+
version "4.5.0"
8501+
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.5.0.tgz#c71c6eed34850c375156c29b1ed45cea2f9aee6b"
8502+
integrity sha512-J3PuSfOKX2y9ryOtWxOcKlizkFWyhCvPAc3hhMKMVSTcPxtWiv9oNzvAZp1HKfuQng32YQduHeX+lRDy2+F6VQ==
8503+
84998504
graphql@^15.3.0:
85008505
version "15.3.0"
85018506
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"

0 commit comments

Comments
 (0)