Skip to content

Commit

Permalink
add support for websocket endpoint with graphql-ws
Browse files Browse the repository at this point in the history
  • Loading branch information
lethot committed May 1, 2021
1 parent 5073062 commit a818a33
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/graphql-playground-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"cuid": "^1.3.8",
"graphiql": "^0.17.5",
"graphql": "^15.3.0",
"graphql-ws": "^4.5.0",
"immutable": "^4.0.0-rc.9",
"isomorphic-fetch": "^2.2.1",
"js-yaml": "^3.10.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Popup from './Popup'
import { throttle } from 'lodash'
import { Button } from './Button'
import { styled, css } from '../styled'
import { createClient } from 'graphql-ws';


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

export default class EndpointPopup extends React.Component<Props, State> {
checkEndpoint = throttle(() => {
if (this.state.endpoint.match(/^https?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?.*$/)) {
if (this.state.endpoint.match(/^(https?|wss?):\/\/\w+(\.\w+)*(:[0-9]+)?\/?.*$/)) {
if (this.state.endpoint.match(/^(wss?)/)) {
const client = createClient({
url: this.state.endpoint,
retryAttempts:0
});
const unsubscribe = client.subscribe(
{
query: `{
__schema {
queryType {
kind
}
}
}`,
},
{
next: () => {
this.setState({ valid: true })
unsubscribe()
},
error: () => {
this.setState({ valid: false });
unsubscribe()
},
complete: () => {},
},
);
return;
}

fetch(this.state.endpoint, {
method: 'post',
headers: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApolloLink, Operation, FetchResult, Observable } from 'apollo-link';
import { print, GraphQLError } from 'graphql';
import { Client } from 'graphql-ws';

export class WebSocketLink extends ApolloLink {

constructor(private client: Client) {
super();
}

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(', '),
),
);
}
},
},
);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ApolloLink, execute } from 'apollo-link'
import { parseHeaders } from '../../components/Playground/util/parseHeaders'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { HttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws'
import { isSubscription } from '../../components/Playground/util/hasSubscription'
import {
takeLatest,
Expand Down Expand Up @@ -38,6 +36,10 @@ import { Session, ResponseRecord } from './reducers'
import { addHistoryItem } from '../history/actions'
import { safely } from '../../utils'
import { set } from 'immutable'
import { SubscriptionClient as SubscriptionClientSTWS } from 'subscriptions-transport-ws'
import { WebSocketLink as WebSocketLinkALW } from 'apollo-link-ws'
import { createClient as createSubscriptionClient, Client as SubscriptionClientGWS } from 'graphql-ws'
import { WebSocketLink as WebSocketLinkGW } from './WebSocketLink'

// tslint:disable
let subscriptionEndpoint
Expand All @@ -50,18 +52,22 @@ export interface LinkCreatorProps {
endpoint: string
headers?: Headers
credentials?: string
subscriptionTransport?: string
}

export interface Headers {
[key: string]: string | number | null
}

const isWSEndpoint = (endpoint: string): boolean => !!endpoint.match(/wss?/);

export const defaultLinkCreator = (
session: LinkCreatorProps,
subscriptionEndpoint?: string,
): { link: ApolloLink; subscriptionClient?: SubscriptionClient } => {
): { link: ApolloLink; subscriptionClient?: SubscriptionClientGWS | SubscriptionClientSTWS } => {

let connectionParams = {}
const { headers, credentials } = session
const { headers, credentials, subscriptionTransport } = session

if (headers) {
connectionParams = { ...headers }
Expand All @@ -73,21 +79,53 @@ export const defaultLinkCreator = (
credentials,
})

if (!subscriptionEndpoint) {
return { link: httpLink }
// ws endpoint => graphql-ws default link
if (isWSEndpoint(session.endpoint)) {
const subscriptionClient = createSubscriptionClient({
retryAttempts: 1000,
retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)),
lazy: true,
connectionParams,
url: session.endpoint,
})

return {
link: new WebSocketLinkGW(subscriptionClient),
subscriptionClient,
}
}

// http endpoint & graphql-ws => default link = http + graphql-ws subscriptions
if (subscriptionTransport === 'graphql-ws') {
const subscriptionClient = createSubscriptionClient({
retryWait: () => new Promise(resolve => setTimeout(resolve, 20000)),
lazy: true,
connectionParams,
url: subscriptionEndpoint || session.endpoint.replace('http', 'ws'),
})

return {
subscriptionClient,
link: new WebSocketLinkGW(subscriptionClient)
}
}

const subscriptionClient = new SubscriptionClient(subscriptionEndpoint, {
timeout: 20000,
lazy: true,
connectionParams,
})
// http endpoint => default link = http + subscriptions-transport-ws subscriptions
const subscriptionClient = new SubscriptionClientSTWS(
subscriptionEndpoint || session.endpoint.replace('http', 'ws'),
{
timeout: 20000,
lazy: true,
connectionParams,
}
)

const webSocketLink = new WebSocketLinkALW(subscriptionClient);

const webSocketLink = new WebSocketLink(subscriptionClient)
return {
link: ApolloLink.split(
operation => isSubscription(operation),
webSocketLink as any,
webSocketLink,
httpLink,
),
subscriptionClient,
Expand All @@ -107,6 +145,12 @@ export function setLinkCreator(newLinkCreator) {

const subscriptions = {}

const isSubscriptionClientSTWS = (
client: SubscriptionClientGWS | SubscriptionClientSTWS
): client is SubscriptionClientSTWS => {
return !!(client as SubscriptionClientSTWS).onDisconnected
}

function* runQuerySaga(action) {
// run the query
const { operationName } = action.payload
Expand All @@ -127,12 +171,14 @@ function* runQuerySaga(action) {
if (session.tracingSupported && session.responseTracingOpen) {
headers = set(headers, 'X-Apollo-Tracing', '1')
}

const lol = {
endpoint: session.endpoint,
headers: {
...settings['request.globalHeaders'],
...headers,
},
subscriptionTransport: settings['subscriptions.protocol'],
credentials: settings['request.credentials'],
}

Expand All @@ -143,15 +189,20 @@ function* runQuerySaga(action) {
const channel = eventChannel(emitter => {
let closed = false
if (subscriptionClient && operationIsSubscription) {
subscriptionClient.onDisconnected(() => {
const onDisconnect = () => {
closed = true
emitter({
error: new Error(
`Could not connect to websocket endpoint ${subscriptionEndpoint}. Please check if the endpoint url is correct.`,
),
})
emitter(END)
})
}
if (isSubscriptionClientSTWS(subscriptionClient)) {
subscriptionClient.onDisconnected(onDisconnect)
} else {
subscriptionClient.on('closed', onDisconnect)
}
}
const subscription = execute(link, operation).subscribe({
next: function(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const defaultSettings: ISettings = {
'schema.polling.interval': 2000,
'tracing.hideTracingResponse': true,
'tracing.tracingSupported': true,
'subscriptions.protocol': 'subscription-transport-ws',
}

// tslint:disable-next-line:max-classes-per-file
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-playground-react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export interface ISettings {
['schema.polling.interval']: number
['tracing.hideTracingResponse']: boolean
['tracing.tracingSupported']: boolean
['subscriptions.protocol']: 'subscription-transport-ws' | 'graphql-ws'
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8463,6 +8463,11 @@ graphql-request@^1.4.0, graphql-request@^1.5.0:
dependencies:
cross-fetch "2.2.2"

graphql-ws@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.5.0.tgz#c71c6eed34850c375156c29b1ed45cea2f9aee6b"
integrity sha512-J3PuSfOKX2y9ryOtWxOcKlizkFWyhCvPAc3hhMKMVSTcPxtWiv9oNzvAZp1HKfuQng32YQduHeX+lRDy2+F6VQ==

graphql@^15.3.0:
version "15.3.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"
Expand Down

0 comments on commit a818a33

Please sign in to comment.