(Work in progress!)
A GraphQL WebSocket server and client to facilitate GraphQL subscriptions.
subscriptions-transport-ws
is an extension for GraphQL, and you can use it with any GraphQL client and server (not only Apollo).
See GitHunt-API and GitHunt-React for an example server and client integration.
Start by installing the package, using Yarn or NPM:
Using Yarn:
$ yarn add subscriptions-transport-ws
Or, using NPM:
$ npm install --save subscriptions-transport-ws
This command also installs this package's dependencies, including
graphql-subscriptions
.
Starting with the server, create a new simple SubscriptionsManager
, with a PubSub
implementation:
import { SubscriptionManager, PubSub } from 'graphql-subscriptions';
const schema = {}; // Replace with your GraphQL schema object
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema,
pubsub
});
Now, use your subscriptionManager
, and create your SubscriptionServer
:
import { createServer } from 'http';
import { SubscriptionServer } from 'subscriptions-transport-ws';
const WS_PORT = 5000;
// Create WebSocket listener server
const websocketServer = createServer((request, response) => {
response.writeHead(404);
response.end();
});
// Bind it to port and start listening
websocketServer.listen(WS_PORT, () => console.log(
`Websocket Server is now running on http://localhost:${WS_PORT}`
));
const subscriptionServer = new SubscriptionServer(
{
onConnect: async (connectionParams) => {
// Implement if you need to handle and manage connection
},
subscriptionManager: subscriptionManager
},
{
server: websocketServer,
path: '/'
}
);
For client side, we will use SubscriptionClient
, and we also need to extend our network interface to use this transport for GraphQL subscriptions:
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
import ApolloClient, {createNetworkInterface} from 'apollo-client';
// Create regular NetworkInterface by using apollo-client's API:
const networkInterface = createNetworkInterface({
uri: 'http://localhost:3000' // Your GraphQL endpoint
});
// Create WebSocket client
const wsClient = new SubscriptionClient(`ws://localhost:5000/`, {
reconnect: true,
connectionParams: {
// Pass any arguments you want for initialization
}
});
// Extend the network interface with the WebSocket
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient
);
// Finally, create your ApolloClient instance with the modified network interface
const apolloClient = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions
});
Now, when you want to use subscriptions in client side, use your ApolloClient
instance, with subscribe
or subscribeToMore
(according to your apollo-client usage):
apolloClient.subscribeToMore({
document: gql`
subscription onNewItem {
newItemCreated {
id
}
}`,
variables: {},
updateQuery: (prev, {subscriptionData}) => {
return; // Modify your store and return new state with the new arrived data
}
});
If you don't use any package/modules loader, you can still use this package, by using unpkg
service, and get the client side package from:
https://unpkg.com/subscriptions-transport-ws@VERSION/browser/client.js
Replace VERSION with the latest version of the package.
You can use this package's power with GraphiQL, and subscribe to live-data stream inside GraphiQL.
If you are using the latest version of graphql-server
flavors (graphql-server-express
, graphql-server-koa
, etc...), you already can use it! Make sure to specify subscriptionsEndpoint
in GraphiQL configuration, and that's it!
For example, graphql-server-express
users need to add the following:
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: `YOUR_SUBSCRIPTION_ENDPOINT_HERE`,
}));
If you are using older version, or another GraphQL server, start by modifying GraphiQL static HTML, and add this package and it's fetcher from CDN:
<script src="//unpkg.com/subscriptions-transport-ws@0.5.4/browser/client.js"></script>
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
Then, create SubscriptionClient
and define the fetcher:
let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('SUBSCRIPTION_WS_URL_HERE', {
reconnect: true
});
let myCustomFetcher = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);
graphQLFetcher
is the default fetcher, and we use it as fallback for non-subscription GraphQL operations.
And replace your GraphiQL creation logic to use the new fetcher:
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: myCustomFetcher, // <-- here
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: ${safeSerialize(queryString)},
response: ${safeSerialize(resultString)},
variables: ${safeSerialize(variablesString)},
operationName: ${safeSerialize(operationName)},
}),
document.body
);
url: string
: url that the client will connect tooptions?: Object
: optional, object to modify default client behaviortimeout?: number
: how long the client should wait in ms for a subscription to be started (default 5000 ms)how long the client should wait in ms for a subscription to be started (default 5000 ms)connectionParams?: Object
: object that will be available as first argument ofonConnect
(in server side)reconnect?: boolean
: automatic reconnect in case of connection errorreconnectionAttempts?: number
: how much reconnect attemptsconnectionCallback?: (error) => {}
: optional, callback that called after the first init message, with the error (if there is one)
webSocketImpl: Object
- optional, WebSocket implementation. use this when your environment does not have a built-in native WebSocket (for example, with NodeJS client)
options: {SubscriptionOptions}
query: string
: GraphQL subscriptionvariables: Object
: GraphQL subscription variablesoperationName: string
: operation name of the subscription
handler: (errors: Error[], result?: any) => void
: function to handle any errors and results from the subscription response
id: string
: the subscription ID of the subscription to unsubscribe from
eventName: string
: the name of the event, available events are:connect
,reconnect
anddisconnect
callback: Function
: function to be called when websocket connects and initialized.thisContext: any
:this
context to use when calling the callback function.- => Returns an
off
method to cancel the event subscription.
callback: Function
: function to be called when websocket connects and initialized.thisContext: any
:this
context to use when calling the callback function.- => Returns an
off
method to cancel the event subscription.
callback: Function
: function to be called when websocket re-connects and initialized.thisContext: any
:this
context to use when calling the callback function.- => Returns an
off
method to cancel the event subscription.
callback: Function
: function to be called when websocket disconnects.thisContext: any
:this
context to use when calling the callback function.- => Returns an
off
method to cancel the event subscription.
networkInterface: any
- network interface to extend withsubscribe
andunsubscribe
methods.wsClient: SubscriptionClient
- network interface to extend withsubscribe
andunsubscribe
methods.
A quick way to add the subscribe
and unsubscribe
functions to the network interface
options: {ServerOptions}
subscriptionManager: SubscriptionManager
: GraphQL subscription manageronSubscribe?: (message: SubscribeMessage, params: SubscriptionOptions, webSocket: WebSocket)
: optional method to create custom params that will be used when resolving this subscriptiononUnsubscribe?: (webSocket: WebSocket)
: optional method that called when a client unsubscribeonConnect?: (connectionParams: Object, webSocket: WebSocket)
: optional method that called when a client connects to the socket, called with theconnectionParams
from the client, if the return value is an object, its elements will be added to the context. returnfalse
or throw an exception to reject the connection. May return a Promise.onDisconnect?: (webSocket: WebSocket)
: optional method that called when a client disconnectskeepAlive?: number
: optional interval in ms to sendKEEPALIVE
messages to all clients
socketOptions: {WebSocket.IServerOptions}
: options to pass to the WebSocket object (full docs here)server?: HttpServer
- existing HTTP server to use (use withouthost
/port
)host?: string
- server hostport?: number
- server portpath?: string
- endpoint path
Each message has a type
field, which defined in the protocol of this package, as well as associated fields depending on the message type.
Client sends this message after connecting, this triggers onConnect
on the server.
payload: Object
: optional parameters that the client specifies inconnectionParams
Client sends this message to start a subscription for a query.
query: GraphQLDocument
: GraphQL subscriptionvariables: Object
: GraphQL subscription variablesoperationName: string
: operation name of the subscriptionid: string
: subscription ID
Client sends this message to end a subscription.
id: string
: subscription ID of the subscription to be terminated
The server sends this message if onConnect
callback returns false
or throws an exception. after sending this message, the server disconnects the client.
payload: Object
: the server side error
The server sends this message if onConnect
callback returns any other value then false
.
The server sends this message to confirm that it has validated the subscription query and is subscribed to the triggers.
id: string
: ID of the subscription that was successfully set up
Server sends this message upon failing to register a subscription. It may also send this message at any point during the subscription to notify the client the the subscription has been stopped.
payload: { errors: Array<Object> }
: payload with an array of errors attributed to the subscription failing on the serverid: string
: subscription ID of the subscription that failed on the server
GraphQL result sent periodically from server to client according to subscription.
payload: GraphQLResult
: GraphQL result from running the subscriptionid: string
: subscription ID
Server message sent periodically to keep the client connection alive.
This is a demonstration of client-server communication, in order to get a better understanding of the protocol flow:
- Client creates a WebSocket instance using
SubscriptionsClient
object. - Client sends
INIT
message to the server. - Server calls
onConnect
callback with the init arguments, waits for init to finish and returns it's return value withINIT_SUCCESS
, orINIT_FAIL
in case offalse
or thrown exception fromonConnect
callback. - Client gets
INIT_SUCCESS
and waits for the client's app to create subscriptions. - App creates a subscription using
subscribe
client's API, and theSUBSCRIPTION_START
message sent to the server. - Server calls
onSubscribe
callback, and responds withSUBSCRIPTION_SUCCESS
in case of zero errors, orSUBSCRIPTION_FAIL
if there is a problem with the subscription. - Client get
SUBSCRIPTION_SUCCESS
and waits for data. - App triggers
PubSub
's publication method, and the server publishes the event, passing it through thegraphql-subscriptions
package for filtering and resolving. - Client receives
SUBSCRIPTION_DATA
with the data, and handles it withapollo-client
instance.