Skip to content

Instantly share code, notes, and snippets.

@trbngr
Last active July 22, 2016 19:06
Show Gist options
  • Select an option

  • Save trbngr/c4ae216d7b4c1f3f6ded08e23084203c to your computer and use it in GitHub Desktop.

Select an option

Save trbngr/c4ae216d7b4c1f3f6ded08e23084203c to your computer and use it in GitHub Desktop.
My root component for a relay-subscriptions application.
import React, {Component, PropTypes} from 'react';
import {SubscriptionProvider} from 'relay-subscriptions'
import uuid from 'uuid'
const outgoing = {
subscribe: 'subscribe',
ping: 'ping'
};
const incoming = {
accepted: 'accepted',
rejected: 'rejected',
payload: 'payload',
pong: 'pong'
};
const emptyRequest = {
clientId: '',
getQueryString: () => '',
getVariables: () => ({}),
dispose: () => {
},
onNext: () => {
},
onError: () => {
},
shouldProcessPayload: () => false
};
class SubscriptionRoot extends Component {
constructor(props, context) {
super(props, context);
this.clientId = uuid();
this.state = { connected: false, reconnect: true };
this.requestStore = new RequestStore();
this.pingIntervalId = undefined;
this.subscribe = ::this.subscribe;
}
static log(...args) {
console.log('[subscription root]:', ...args);
}
getChildContext() {
return {
auth: this.props.auth
};
}
componentDidMount() {
SubscriptionRoot.log('mounting');
this.connect();
}
connect() {
this.socket = new WebSocket(GRAPHQL_WS);
this.socket.onmessage = ({ data }) => {
const payload = JSON.parse(data);
if (!payload)
return;
this.handlePayload(payload)
};
this.socket.onopen = () => {
this.pingIntervalId = setInterval(() => this.send(outgoing.ping), GRAPHQL_WS_PING_INTERVAL);
this.setState({ connected: true }, () => {
SubscriptionRoot.log('connected to graphql subscriptions.');
});
};
this.socket.onclose = () => {
clearInterval(this.pingIntervalId);
this.setState({ connected: false }, () => {
SubscriptionRoot.log('disconnected from graphql subscriptions.');
var reconnect = this.state.reconnect;
if (reconnect) {
SubscriptionRoot.log('trying to reconnect');
this.connect();
} else {
this.requestStore.dispose();
}
});
};
this.socket.onerror = error => {
console.warn(error);
this.requestStore.raiseSocketError(error);
};
}
componentWillUnmount() {
this.setState({ reconnect: false }, () => this.socket.close(0, "unmounting"))
}
send(type, request = emptyRequest) {
if (!this.state.connected) {
console.warn(`Not connected. We shouldn't see this. Maybe we can add some reconnect strategy?`);
return;
}
const { auth } = this.props;
this.socket.send(JSON.stringify({
type,
clientId: this.clientId,
queryId: this.requestStore.add(request),
query: request.getQueryString(),
variables: JSON.stringify(request.getVariables() || {}),
token: auth.getToken()
}));
}
handlePayload(payload) {
switch (payload.type) {
case incoming.accepted:
SubscriptionRoot.log(`${this.requestStore.getDebugName(payload.queryId)} may live.`);
break;
case incoming.rejected:
SubscriptionRoot.log(`${this.requestStore.getDebugName(payload.queryId)} has been reject. reason: '${payload.message}' removing subscription.`);
this.requestStore.remove(payload.queryId);
break;
case incoming.payload:
const request = this.requestStore.get(payload.queryId);
if (!request) {
SubscriptionRoot.log('No request found for subscription payload. Discarding', payload);
return;
}
SubscriptionRoot.log(`got payload for '${request.getDebugName()}'. dispatching...`, payload.message.data);
if (payload.clientId !== this.clientId) {
if (!!request.shouldProcessPayload(payload.message.data)) {
request.onNext(payload.message.data)
}
}
break;
case incoming.pong:
default:
SubscriptionRoot.log('unsupported payload', payload);
break;
}
}
subscribe(request, options = {
shouldProcessPayload: () => {
}
}) {
request.setDisposable({
dispose: () => {
SubscriptionRoot.log('dispose', request);
}
});
this.send(outgoing.subscribe, Object.assign(request, options));
}
render() {
return (
<SubscriptionProvider environment={this.props.environment} subscribe={this.subscribe}>
{this.props.children}
</SubscriptionProvider>
);
}
}
class RequestStore {
constructor() {
this.store = {};
}
add(request) {
const id = uuid();
this.store = Object.assign(this.store, { [id]: request });
return id;
}
remove(id) {
this.store = Object.keys(this.store)
.filter(key => key !== id)
.map(key => ({ key: this.store[key] }));
}
dispose() {
Object.keys(this.store).forEach(key => this.store[key].dispose());
}
get(id) {
return this.store[id];
}
getDebugName(id) {
const request = this.store[id];
return request ? request.getDebugName() : '';
}
raiseSocketError(e) {
Object.keys(this.store).forEach(key => this.store[key].onError(e));
}
}
SubscriptionRoot.propTypes = {
auth: PropTypes.object.isRequired,
environment: PropTypes.object.isRequired,
children: PropTypes.element
};
SubscriptionRoot.childContextTypes = {
auth: PropTypes.object.isRequired
};
SubscriptionRoot.defaultProps = {};
export default SubscriptionRoot;
@trbngr

trbngr commented Jul 22, 2016

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment