Commit dc169f99 authored by Robert Knight's avatar Robert Knight

Modernize WebSocket wrapper

Modernize our auto-reconnecting wrapper for `WebSocket` to match current
conventions in the client and improve the documentation.

 - Use named rather than default exports
 - Convert functions to arrow functions and remove unneeded `self`
   alias.
 - Add JSDoc comments and types

A circular reference between the `connect` and `onAbnormalClose`
functions was broken by adding a second argument to `onAbnormalClose`.
parent 16d81f5f
...@@ -2,7 +2,7 @@ import * as queryString from 'query-string'; ...@@ -2,7 +2,7 @@ import * as queryString from 'query-string';
import warnOnce from '../../shared/warn-once'; import warnOnce from '../../shared/warn-once';
import { generateHexString } from '../util/random'; import { generateHexString } from '../util/random';
import Socket from '../websocket'; import { Socket } from '../websocket';
import { watch } from '../util/watch'; import { watch } from '../util/watch';
/** /**
......
...@@ -129,7 +129,7 @@ describe('StreamerService', () => { ...@@ -129,7 +129,7 @@ describe('StreamerService', () => {
$imports.$mock({ $imports.$mock({
'../../shared/warn-once': fakeWarnOnce, '../../shared/warn-once': fakeWarnOnce,
'../websocket': FakeSocket, '../websocket': { Socket: FakeSocket },
}); });
}); });
......
import Socket, { import {
Socket,
CLOSE_NORMAL, CLOSE_NORMAL,
CLOSE_GOING_AWAY, CLOSE_GOING_AWAY,
CLOSE_ABNORMAL, CLOSE_ABNORMAL,
......
...@@ -27,31 +27,73 @@ const RECONNECT_MIN_DELAY = 1000; ...@@ -27,31 +27,73 @@ const RECONNECT_MIN_DELAY = 1000;
* - Uses the standard EventEmitter API for reporting open, close, error * - Uses the standard EventEmitter API for reporting open, close, error
* and message events. * and message events.
*/ */
export default class Socket extends TinyEmitter { export class Socket extends TinyEmitter {
/**
* Connect to the WebSocket endpoint at `url`.
*
* @param {string} url
*/
constructor(url) { constructor(url) {
super(); super();
const self = this; /**
* Queue of JSON objects which have not yet been submitted
// queue of JSON objects which have not yet been submitted *
* @type {object[]}
*/
const messageQueue = []; const messageQueue = [];
// the current WebSocket instance /**
* The active `WebSocket` instance
*
* @type {WebSocket}
*/
let socket; let socket;
// a pending operation to connect a WebSocket // a pending operation to connect a WebSocket
let operation; let operation;
function sendMessages() { const sendMessages = () => {
while (messageQueue.length > 0) { while (messageQueue.length > 0) {
const messageString = JSON.stringify(messageQueue.shift()); const messageString = JSON.stringify(messageQueue.shift());
socket.send(messageString); socket.send(messageString);
} }
};
/**
* Handler for when the WebSocket disconnects "abnormally".
*
* This may be the result of a failure to connect, or an abnormal close after
* a previous successful connection.
*
* @param {Error} error
* @param {() => void} reconnect
*/
const onAbnormalClose = (error, reconnect) => {
// If we're already in a reconnection loop, trigger a retry...
if (operation) {
if (!operation.retry(error)) {
console.error(
'reached max retries attempting to reconnect websocket'
);
}
return;
} }
// ...otherwise reconnect the websocket after a short delay.
let delay = RECONNECT_MIN_DELAY;
delay += Math.floor(Math.random() * delay);
operation = setTimeout(() => {
operation = null;
reconnect();
}, delay);
};
// Connect the websocket immediately. If a connection attempt is already in /**
// progress, do nothing. * Connect the WebSocket.
function connect() { *
* If a connection attempt is already in progress, do nothing.
*/
const connect = () => {
if (operation) { if (operation) {
return; return;
} }
...@@ -64,62 +106,35 @@ export default class Socket extends TinyEmitter { ...@@ -64,62 +106,35 @@ export default class Socket extends TinyEmitter {
randomize: true, randomize: true,
}); });
operation.attempt(function () { operation.attempt(() => {
socket = new WebSocket(url); socket = new WebSocket(url);
socket.onopen = function (event) { socket.onopen = event => {
onOpen(); operation = null;
self.emit('open', event); sendMessages();
this.emit('open', event);
}; };
socket.onclose = function (event) { socket.onclose = event => {
if (event.code === CLOSE_NORMAL || event.code === CLOSE_GOING_AWAY) { if (event.code === CLOSE_NORMAL || event.code === CLOSE_GOING_AWAY) {
self.emit('close', event); this.emit('close', event);
return; return;
} }
const err = new Error( const err = new Error(
`WebSocket closed abnormally, code: ${event.code}` `WebSocket closed abnormally, code: ${event.code}`
); );
console.warn(err); console.warn(err);
onAbnormalClose(err); onAbnormalClose(err, connect);
}; };
socket.onerror = function (event) { socket.onerror = event => {
self.emit('error', event); this.emit('error', event);
}; };
socket.onmessage = function (event) { socket.onmessage = event => {
self.emit('message', event); this.emit('message', event);
}; };
}); });
} };
// onOpen is called when a websocket connection is successfully established.
function onOpen() {
operation = null;
sendMessages();
}
// onAbnormalClose is called when a websocket connection closes abnormally.
// This may be the result of a failure to connect, or an abnormal close after
// a previous successful connection.
function onAbnormalClose(error) {
// If we're already in a reconnection loop, trigger a retry...
if (operation) {
if (!operation.retry(error)) {
console.error(
'reached max retries attempting to reconnect websocket'
);
}
return;
}
// ...otherwise reconnect the websocket after a short delay.
let delay = RECONNECT_MIN_DELAY;
delay += Math.floor(Math.random() * delay);
operation = setTimeout(function () {
operation = null;
connect();
}, delay);
}
/** Close the underlying WebSocket connection */ /** Close the underlying WebSocket connection */
this.close = function () { this.close = () => {
// nb. Always sent a status code in the `close()` call to work around // nb. Always sent a status code in the `close()` call to work around
// a problem in the backend's ws4py library. // a problem in the backend's ws4py library.
// //
...@@ -139,8 +154,10 @@ export default class Socket extends TinyEmitter { ...@@ -139,8 +154,10 @@ export default class Socket extends TinyEmitter {
/** /**
* Send a JSON object via the WebSocket connection, or queue it * Send a JSON object via the WebSocket connection, or queue it
* for later delivery if not currently connected. * for later delivery if not currently connected.
*
* @param {object} message
*/ */
this.send = function (message) { this.send = message => {
messageQueue.push(message); messageQueue.push(message);
if (this.isConnected()) { if (this.isConnected()) {
sendMessages(); sendMessages();
...@@ -148,7 +165,7 @@ export default class Socket extends TinyEmitter { ...@@ -148,7 +165,7 @@ export default class Socket extends TinyEmitter {
}; };
/** Returns true if the WebSocket is currently connected. */ /** Returns true if the WebSocket is currently connected. */
this.isConnected = function () { this.isConnected = () => {
return socket.readyState === WebSocket.OPEN; return socket.readyState === WebSocket.OPEN;
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment