@selfcommunity/utils
Version:
Utilities to integrate a Community.
241 lines (240 loc) • 7.65 kB
JavaScript
/**
* WSClient: manage socket connection
* @param options
* @constructor
*/
export default class WSClient {
/**
* Constructor
* @param cfg
*/
constructor(cfg) {
this._attempts = 1;
this._heartbeatInterval = null;
this._missedHeartbeats = 0;
if (!this.isValidOptions(cfg)) {
return;
}
this._cfg = Object.assign({}, { heartbeatMsg: null, debug: false, mustReconnect: true }, cfg);
this.connect();
}
/**
* Get instance
*/
static getInstance(cfg) {
this._instance = this._instance || new WSClient(cfg);
return this._instance;
}
/**
* Connect
*/
connect() {
try {
if (this._ws && (this.isConnecting() || this.isConnected())) {
// There is already a connection
this._cfg.debug && console.info('Websocket is connecting or already connected.');
return;
}
// Callback 'connecting' if exist
typeof this._cfg.connecting === 'function' && this._cfg.connecting();
this._cfg.debug && console.info(`Connecting to ${this._cfg.uri} ...`);
// Open the connection
this._ws = new WebSocket(this._cfg.uri, this._cfg.protocols);
this._ws.onopen = this.onOpen.bind(this);
this._ws.onmessage = this.onMessage.bind(this);
this._ws.onerror = this.onError.bind(this);
this._ws.onclose = this.onClose.bind(this);
this._timer = null;
}
catch (err) {
console.error(err);
this.tryToReconnect();
}
}
/**
* Validate options
* @param cfg
*/
isValidOptions(cfg) {
let _error = false;
if (!cfg) {
console.error('Invalid WSClient options.');
return _error;
}
if (!cfg.uri) {
console.error('Invalid WSClient Uri options.');
_error = true;
}
if (cfg && cfg.connecting && !(typeof cfg.connecting === 'function')) {
console.error('Invalid WSClient connecting options.');
_error = true;
}
if (cfg && cfg.connected && !(typeof cfg.connected === 'function')) {
console.error('Invalid WSClient connected options.');
_error = true;
}
if (cfg && cfg.receiveMessage && !(typeof cfg.receiveMessage === 'function')) {
console.error('Invalid WSClient receiveMessage options.');
_error = true;
}
if (cfg && cfg.disconnected && !(typeof cfg.disconnected === 'function')) {
console.error('Invalid WSClient connecting options.');
_error = true;
}
if (cfg && cfg.heartbeatMsg && !(typeof cfg.heartbeatMsg === 'string')) {
console.error('Invalid WSClient heartbeatMsg options.');
_error = true;
}
if (cfg && cfg.debug && !(typeof cfg.debug === 'boolean')) {
console.error('Invalid WSClient debug options.');
_error = true;
}
return !_error;
}
/**
* Try to reconnect if previous connection failed
* Generate an interval, after that try to reconnect
*/
tryToReconnect() {
if (this._cfg.mustReconnect && !this._timer) {
this._cfg.debug && console.info(`Reconnecting...`);
let interval = this.generateInterval(this._attempts);
this._timer = setTimeout(this.reconnect.bind(this), interval);
}
}
/**
* Reestablish the connection
* Increase the number of attempts
*/
reconnect() {
this._attempts++;
this.connect();
}
/**
* Send heartbeat every 5 seconds
* If missing more than 3 heartbeats close connection
*/
sendHeartbeat() {
try {
this._missedHeartbeats++;
if (this._missedHeartbeats > 3)
throw new Error('Too many missed heartbeats.');
this._ws.send(this._cfg.heartbeatMsg);
}
catch (e) {
clearInterval(this._heartbeatInterval);
this._heartbeatInterval = null;
this._cfg.debug && console.warn(`Closing connection. Reason: ${e.message}`);
if (!this.isClosing() && !this.isClosed()) {
this.close();
}
}
}
/**
* Established the new connection
* Reset this._attempts counter
*/
onOpen() {
this._cfg.debug && console.info('Connected!');
this._attempts = 1;
if (this._cfg.heartbeatMsg && this._heartbeatInterval === null) {
this._missedHeartbeats = 0;
this._heartbeatInterval = setInterval(this.sendHeartbeat.bind(this), 5000);
}
typeof this._cfg.connected === 'function' && this._cfg.connected();
}
/**
* Connection closed. Try to reconnect.
* @param evt
*/
onClose(evt) {
this._cfg.debug && console.info('Connection closed!');
typeof this._cfg.disconnected === 'function' && this._cfg.disconnected(evt);
this.tryToReconnect();
}
/**
* An error occured
* @param evt
*/
onError(evt) {
this._cfg.debug && console.error('Websocket connection is broken!');
this._cfg.debug && console.error(evt);
}
/**
* A message has arrived.
* If it is the heartbeat -> reset this._missedHeartbeats
* If it is data pass data to the callback
* @param evt
*/
onMessage(evt) {
if (this._cfg.heartbeatMsg && evt.data === this._cfg.heartbeatMsg) {
// reset the counter for missed heartbeats
this._missedHeartbeats = 0;
}
else if (typeof this._cfg.receiveMessage === 'function') {
return this._cfg.receiveMessage(evt.data);
}
}
/**
* Generate an interval that is randomly between 0 and 2^k - 1, where k is
* the number of connection attmpts, with a maximum interval of 30 seconds,
* so it starts at 0 - 1 seconds and maxes out at 0 - 30 seconds
*/
generateInterval(k) {
let maxInterval = (Math.pow(2, k) - 1) * 1000;
// If the generated interval is more than 30 seconds, truncate it down to 30 seconds.
if (maxInterval > 30 * 1000) {
maxInterval = 30 * 1000;
}
// generate the interval to a random number between 0 and the maxInterval determined from above
return Math.random() * maxInterval;
}
/**
* Send message
* @param message
*/
sendMessage(message) {
this._ws && this._ws.send(message);
}
/**
* Get the ws state
*/
getState() {
return this._ws && this._ws.readyState;
}
/**
* Check if ws is in connecting state
*/
isConnecting() {
return this._ws && this._ws.readyState === 0;
}
/**
* Check if ws is connected
*/
isConnected() {
return this._ws && this._ws.readyState === 1;
}
/**
* Check if ws is in closing state
*/
isClosing() {
return this._ws && this._ws.readyState === 2;
}
/**
* Check if ws is closed
*/
isClosed() {
return this._ws && this._ws.readyState === 3;
}
/**
* Close the connection
*/
close() {
clearInterval(this._heartbeatInterval);
this._cfg.mustReconnect = false;
if (!this.isClosing() || !this.isClosed()) {
this._ws.close();
this._cfg.debug && console.error('Websocket closed.');
}
}
}