stompit
Version:
STOMP client library for node.js
314 lines (215 loc) • 7.71 kB
JavaScript
/*jslint node: true, indent: 2, unused: true, maxlen: 80, camelcase: true, esversion: 9 */
const { EventEmitter } = require('events');
const { format } = require('util');
const connect = require('./connect');
const getAddressInfo = require('./connect-failover/getAddressInfo');
const parseFailoverUri = require('./connect-failover/parseFailoverUri');
const parseServerUri = require('./connect-failover/parseServerUri');
class ConnectFailover extends EventEmitter {
constructor(servers, options) {
super();
const defaults = {
// Milliseconds delay of the first reconnect
initialReconnectDelay: 10,
// Maximum milliseconds delay of any reconnect
maxReconnectDelay: 30000,
// Exponential increase of the reconnect delay
useExponentialBackOff: true,
// The exponent used in the exponential backoff attempts
reconnectDelayExponent: 2.0,
// Maximum number of reconnects
maxReconnects: -1,
// Randomly choose a server to use for reconnect
randomize: true,
// Override the connect function
connectFunction: connect
};
options = {...defaults, ...options};
switch (typeof servers) {
case 'undefined':
servers = [{}]; // default server
break;
case 'string':
const uriConfig = parseFailoverUri(servers);
servers = uriConfig.servers;
Object.assign(options, uriConfig.options);
break;
default:
break;
}
for (let key in defaults) {
this[key] = options[key];
}
this._defaultConnectOptions = typeof options.connect === 'object' ?
options.connect : {};
this._connect = options.connectFunction;
this._servers = servers.map(this._createServer.bind(this));
}
_createServer(config) {
const connectOptions = this._normalizeConnectOptions(config);
const server = {
connectOptions: connectOptions,
remoteAddress: getAddressInfo(connectOptions)
};
return server;
}
_normalizeConnectOptions(arg) {
const config = Object.assign({
host: 'localhost',
port: 61613
}, this._defaultConnectOptions);
switch (typeof arg) {
case 'string':
// extend connectHeaders object
const serverUriConfig = parseServerUri(arg);
if (serverUriConfig.connectHeaders && config.connectHeaders) {
Object.assign( serverUriConfig.connectHeaders,
config.connectHeaders,
serverUriConfig.connectHeaders );
}
Object.assign(config, serverUriConfig);
break;
case 'object':
Object.assign(config, arg);
break;
default:
const type = typeof arg;
throw new Error(`invalid type (${type}) for server config argument`);
}
return config;
}
addServer() {
this._servers.push(this._createServer.apply(this, arguments));
}
getReconnectDelay(reconnects) {
return Math.min(
this.initialReconnectDelay * (
this.useExponentialBackOff ?
Math.pow(this.reconnectDelayExponent, reconnects) - 1
: Math.min(reconnects, 1)),
this.maxReconnectDelay
);
}
_initConnectState() {
function ConnectState(serverProperties) {
this.serverProperties = serverProperties;
this.failedConnects = 0;
this._blacklisted = false;
this._blacklistError = null;
}
// Public API method
ConnectState.prototype.blacklist = function(error) {
if (this._blacklisted) {
return;
}
this._blacklisted = true;
this._blacklistError = error;
};
ConnectState.prototype.isBlacklisted = function() {
return this._blacklisted;
};
ConnectState.prototype.getBlacklistError = function() {
return this._blacklistError;
};
const servers = this._servers.map(function(server) {
return new ConnectState(server);
});
// Shuffle the alternative servers
if (this.randomize === true && servers.length > 2) {
for (let i = 1; i < servers.length; i++) {
const server = servers[i];
const random = 1 + Math.round(Math.random() * (servers.length - 2));
servers[i] = servers[random];
servers[random] = server;
}
}
return servers;
}
connect(callback) {
const servers = this._initConnectState();
const blacklist = [];
if (servers.length === 0) {
callback(new Error('no server addresses'));
return;
}
const self = this;
const doConnect = this._connect;
let connectingClient;
const maxReconnects = this.maxReconnects;
let aborted = false;
let reconnectDelayTimeout;
function abort() {
aborted = true;
clearTimeout(reconnectDelayTimeout);
if (connectingClient) {
connectingClient.getTransportSocket().destroy();
}
}
function connect() {
if (aborted) {
return;
}
const server = servers[0];
const onConnected = function(error, client) {
connectingClient = null;
if (aborted) {
if (!error) {
client.destroy();
}
return;
}
if (error) {
if (self.listeners('error').length > 0) {
error.connectArgs = server.serverProperties.connectOptions;
self.emit('error', error, server);
}
// Server is sending an ERROR frame due to bad connect headers.
if (typeof error.isApplicationError === 'function' &&
error.isApplicationError()) {
server.blacklist(error);
}
server.failedConnects += 1;
reconnect();
return;
}
server.failedConnects = 0;
self.emit('connect', server);
callback(null, client, reconnect, server);
};
connectingClient =
doConnect(server.serverProperties.connectOptions, onConnected);
self.emit('connecting', server);
}
function reconnect() {
if (aborted) {
return;
}
const server = servers[0];
if (maxReconnects !== -1 && server.failedConnects >= maxReconnects) {
server.blacklist(new Error(format(
'too many failed connects (%d)', server.failedConnects
)));
}
servers.shift();
if (!server.isBlacklisted()) {
servers.push(server);
}
else{
blacklist.push(server);
}
if (servers.length === 0) {
const error = blacklist.length == 1 ? blacklist[0].getBlacklistError() :
new Error('exhausted connection failover');
callback(error);
return;
}
reconnectDelayTimeout = setTimeout(
connect, self.getReconnectDelay(servers[0].failedConnects));
}
connect();
return {
abort: abort
};
}
}
module.exports = ConnectFailover;