Skip to content

Instantly share code, notes, and snippets.

@ForsakenHarmony
Last active July 10, 2018 15:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ForsakenHarmony/e2b29f9164618458a23b3671dab20dcf to your computer and use it in GitHub Desktop.
Save ForsakenHarmony/e2b29f9164618458a23b3671dab20dcf to your computer and use it in GitHub Desktop.
Graphql over websockets
import { watch as watchOnline } from 'is-offline';
import Sockette from 'sockette';
const msg_types = {
GQL_CONNECTION_INIT: 'connection_init', // Client -> Server
GQL_CONNECTION_ACK: 'connection_ack', // Server -> Client
GQL_CONNECTION_ERROR: 'connection_error', // Server -> Client
GQL_CONNECTION_TERMINATE: 'connection_terminate', // Client -> Server
GQL_START: 'start', // Client -> Server
GQL_DATA: 'data', // Server -> Client
GQL_ERROR: 'error', // Server -> Client
GQL_COMPLETE: 'complete', // Server -> Client
GQL_STOP: 'stop', // Client -> Server
};
const url = process.env.WS_URL;
export default function subscriber(token) {
const subscriptions = {};
let open = false;
let reconnecting = false;
let lastid = 0;
const genId = () => String(lastid++);
let buffer = [];
const socket = new Sockette(url, {
protocols: 'graphql-ws',
timeout: 5e3,
maxAttempts: 10,
onopen: () => {
open = true;
const message = {
type: msg_types.GQL_CONNECTION_INIT,
payload: {
Authorization: `Bearer ${token}`,
},
};
socket.json(message);
buffer.forEach(m => socket.json(m));
if (reconnecting) {
Object.values(subscriptions).forEach(s => socket.json(s.message));
}
reconnecting = false;
},
onmessage: event => {
const data = JSON.parse(event.data);
const sub = subscriptions[data.id];
switch (data.type) {
case msg_types.GQL_CONNECTION_ACK: {
console.log('init_success, the handshake is complete');
break;
}
case msg_types.GQL_CONNECTION_ERROR: {
console.error('init_fail returned from WebSocket server');
break;
}
case msg_types.GQL_DATA: {
sub && sub.handler(data.payload.data);
break;
}
case msg_types.GQL_COMPLETE: {
delete subscriptions[data.id];
break;
}
case msg_types.GQL_ERROR: {
sub && sub.handler(void 0, data.payload.errors);
break;
}
}
},
onreconnect: () => {
reconnecting = true;
},
onmaximum: e => {
console.log('Stop Attempting!', e);
},
onclose: () => {
open = false;
},
onerror: e => {
console.error('WS Error:', e);
},
});
watchOnline(online => {
if (!online && open) {
socket.close(1000, 'closed');
} else if (online) {
socket.reconnect();
}
});
function push_msg(msg) {
if (!open) return buffer.push(msg);
socket.json(msg);
}
function request(query, vars, handler) {
const id = genId();
const message = {
id,
type: msg_types.GQL_START,
payload: query(vars),
};
subscriptions[id] = {
message,
handler,
};
push_msg(message);
return () => {
const message = {
id,
type: msg_types.GQL_STOP,
};
delete subscriptions[id];
push_msg(message);
};
}
return {
query(query, vars) {
return new Promise((res, rej) => {
request(query, vars, (d, e) => {
if (e) rej(e);
if (d) res(d);
});
});
},
subscribe(query, vars, handler) {
if (typeof handler !== 'function') {
throw new Error('handler has to be a function');
}
return request(query, vars, handler);
},
close() {
Object.values(subscriptions).forEach(({ message: { id } }) => {
const message = {
id,
type: msg_types.GQL_STOP,
};
push_msg(message);
});
push_msg({
type: msg_types.GQL_CONNECTION_TERMINATE,
});
// socket.close(1000, 'closed');
},
};
}
const getOpname = /(query|mutation|subsciption) ?([\w\d-_]+)? ?\(.*?\)? \{/;
export function gql(str) {
str = Array.isArray(str) ? str.join('') : str;
const name = getOpname.exec(str);
return function(variables) {
const data = { query: str };
if (variables) data.variables = variables;
if (name && name.length) {
const operationName = name[2];
if (operationName) data.operationName = name[2];
}
return data;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment