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';
import warnOnce from '../../shared/warn-once';
import { generateHexString } from '../util/random';
import Socket from '../websocket';
import { Socket } from '../websocket';
import { watch } from '../util/watch';
/**
......
......@@ -129,7 +129,7 @@ describe('StreamerService', () => {
$imports.$mock({
'../../shared/warn-once': fakeWarnOnce,
'../websocket': FakeSocket,
'../websocket': { Socket: FakeSocket },
});
});
......
import Socket, {
import {
Socket,
CLOSE_NORMAL,
CLOSE_GOING_AWAY,
CLOSE_ABNORMAL,
......
......@@ -27,31 +27,73 @@ const RECONNECT_MIN_DELAY = 1000;
* - Uses the standard EventEmitter API for reporting open, close, error
* 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) {
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 = [];
// the current WebSocket instance
/**
* The active `WebSocket` instance
*
* @type {WebSocket}
*/
let socket;
// a pending operation to connect a WebSocket
let operation;
function sendMessages() {
const sendMessages = () => {
while (messageQueue.length > 0) {
const messageString = JSON.stringify(messageQueue.shift());
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.
function connect() {
/**
* Connect the WebSocket.
*
* If a connection attempt is already in progress, do nothing.
*/
const connect = () => {
if (operation) {
return;
}
......@@ -64,62 +106,35 @@ export default class Socket extends TinyEmitter {
randomize: true,
});
operation.attempt(function () {
operation.attempt(() => {
socket = new WebSocket(url);
socket.onopen = function (event) {
onOpen();
self.emit('open', event);
socket.onopen = event => {
operation = null;
sendMessages();
this.emit('open', event);
};
socket.onclose = function (event) {
socket.onclose = event => {
if (event.code === CLOSE_NORMAL || event.code === CLOSE_GOING_AWAY) {
self.emit('close', event);
this.emit('close', event);
return;
}
const err = new Error(
`WebSocket closed abnormally, code: ${event.code}`
);
console.warn(err);
onAbnormalClose(err);
onAbnormalClose(err, connect);
};
socket.onerror = function (event) {
self.emit('error', event);
socket.onerror = event => {
this.emit('error', event);
};
socket.onmessage = function (event) {
self.emit('message', event);
socket.onmessage = 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 */
this.close = function () {
this.close = () => {
// nb. Always sent a status code in the `close()` call to work around
// a problem in the backend's ws4py library.
//
......@@ -139,8 +154,10 @@ export default class Socket extends TinyEmitter {
/**
* Send a JSON object via the WebSocket connection, or queue it
* for later delivery if not currently connected.
*
* @param {object} message
*/
this.send = function (message) {
this.send = message => {
messageQueue.push(message);
if (this.isConnected()) {
sendMessages();
......@@ -148,7 +165,7 @@ export default class Socket extends TinyEmitter {
};
/** Returns true if the WebSocket is currently connected. */
this.isConnected = function () {
this.isConnected = () => {
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