Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ws4redis.js #287

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 215 additions & 165 deletions ws4redis/static/js/ws4redis.js
Original file line number Diff line number Diff line change
@@ -1,167 +1,217 @@
/**
* options.uri - > The Websocket URI
* options.connected -> Callback called after the websocket is connected.
* options.connecting -> Callback called when the websocket is connecting.
* options.disconnected -> Callback called after the websocket is disconnected.
* options.receive_message -> Callback called when a message is received from the websocket.
* options.heartbeat_msg -> String to identify the heartbeat message.
* $ -> JQuery instance.
*/
function WS4Redis(options, $) {
'use strict';
var opts, ws, deferred, timer, attempts = 1, must_reconnect = true;
var heartbeat_interval = null, missed_heartbeats = 0;

if (this === undefined)
return new WS4Redis(options, $);
if (options.uri === undefined)
throw new Error('No Websocket URI in options');
if ($ === undefined)
$ = jQuery;
opts = $.extend({ heartbeat_msg: null }, options);
connect(opts.uri);

function connect(uri) {
try {
if (ws && (is_connecting() || is_connected())) {
console.log("Websocket is connecting or already connected.");
return;
}

if ($.type(opts.connecting) === 'function') {
opts.connecting();
}

console.log("Connecting to " + uri + " ...");
deferred = $.Deferred();
ws = new WebSocket(uri);
ws.onopen = on_open;
ws.onmessage = on_message;
ws.onerror = on_error;
ws.onclose = on_close;
timer = null;
} catch (err) {
try_to_reconnect();
deferred.reject(new Error(err));
}
}

function try_to_reconnect() {
if (must_reconnect && !timer) {
// try to reconnect
console.log('Reconnecting...');
var interval = generate_inteval(attempts);
timer = setTimeout(function() {
attempts++;
connect(ws.url);
}, interval);
}
}

function send_heartbeat() {
try {
missed_heartbeats++;
if (missed_heartbeats > 3)
throw new Error("Too many missed heartbeats.");
ws.send(opts.heartbeat_msg);
} catch(e) {
clearInterval(heartbeat_interval);
heartbeat_interval = null;
console.warn("Closing connection. Reason: " + e.message);
if ( !is_closing() && !is_closed() ) {
ws.close();
}
}
}

function on_open() {
console.log('Connected!');
// new connection, reset attemps counter
attempts = 1;
deferred.resolve();
if (opts.heartbeat_msg && heartbeat_interval === null) {
missed_heartbeats = 0;
heartbeat_interval = setInterval(send_heartbeat, 5000);
}
if ($.type(opts.connected) === 'function') {
opts.connected();
}
}

function on_close(evt) {
console.log("Connection closed!");
if ($.type(opts.disconnected) === 'function') {
opts.disconnected(evt);
}
try_to_reconnect();
}

function on_error(evt) {
console.error("Websocket connection is broken!");
deferred.reject(new Error(evt));
}

function on_message(evt) {
if (opts.heartbeat_msg && evt.data === opts.heartbeat_msg) {
// reset the counter for missed heartbeats
missed_heartbeats = 0;
} else if ($.type(opts.receive_message) === 'function') {
return opts.receive_message(evt.data);
}
}

// this code is borrowed from http://blog.johnryding.com/post/78544969349/
//
// 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
function generate_inteval(k) {
var 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;
}

this.send_message = function(message) {
ws.send(message);
};

this.get_state = function() {
return ws.readyState;
};

function is_connecting() {
return ws && ws.readyState === 0;
}

function is_connected() {
return ws && ws.readyState === 1;
}

function is_closing() {
return ws && ws.readyState === 2;
}

function is_closed() {
return ws && ws.readyState === 3;
}


this.close = function () {
clearInterval(heartbeat_interval);
must_reconnect = false;
if (!is_closing() || !is_closed()) {
ws.close();
}
}

this.is_connecting = is_connecting;
this.is_connected = is_connected;
this.is_closing = is_closing;
this.is_closed = is_closed;
* options & defaults
- maxAttempts: 50,
- mustReconnect: true,
- uri: null,
- heartbeatMsg: null,
- maxMissedHeartbeats: 5,
- heartbeatLapse: 5000,
- autoConnect: true,
- receiveMessage (notification, evt) {
console.log('ws4redis receive message', evt)
},
- connecting () {
console.log('ws4redis connecting...')
},
- connected () {
console.log('ws4redis connected')
},
- reconnecting (attempts, time, e) {
console.log('ws4redis Reconnect attept ' + attempts + ' on ' + time, e)
},
- error (evt) {
console.error('ws4redis error', evt)
},
- heartbeatTimeout (e) {
console.error('ws4redis heartbeat timeot, closing', e)
},
- disconnected (evt) {
console.error('disconnected', evt)
}
*/

const generateInteval = function (k) {
const max = 30 * 1000
const interval = (Math.pow(2, k) - 1) * 1000
return interval > max ? max : interval // Math.random() * <- for randomly factor
}

class WS4Redis {
constructor (options) {
// Retrofix options params:
if (options.receive_message) {
options.receiveMessage = options.receive_message
delete options.receive_message
}
if (options.heartbeat_msg) {
options.heartbeatMsg = options.heartbeat_msg
delete options.heartbeat_msg
}

const self = this
self.options = Object.assign({
maxAttempts: 50,
mustReconnect: true,
uri: null,
heartbeatMsg: null,
maxMissedHeartbeats: 5,
heartbeatLapse: 5000,
autoConnect: true,
receiveMessage (notification, evt) {
console.log('ws4redis receive message', evt)
},
connecting () {
console.log('ws4redis connecting...')
},
connected () {
console.log('ws4redis connected')
},
reconnecting (attempts, time, e) {
console.log('ws4redis Reconnect attept ' + attempts + ' on ' + time, e)
},
error (evt) {
console.error('ws4redis error', evt)
},
heartbeatTimeout (e) {
console.error('ws4redis heartbeat timeot, closing', e)
},
disconnected (evt) {
console.error('disconnected', evt)
}
}, options)
self.attempts = 0
self.ws = null
self.timer = null
self.heartbeatInterval = null
self.missedHeartbeats = 0
self.forcedClose = false

if (!self.options.uri) { throw new Error('No Websocket URI in options') }

if (self.options.autoConnect) {
self.connect()
}
}

connect () {
const self = this
try {
if (self.ws && (self.isConnecting() || self.isConnected())) {
console.warn('Websocket is connecting or already connected.')
return
}

if (typeof self.options.connecting === 'function') {
self.options.connecting()
}

const ws = new WebSocket(self.options.uri)
ws.onopen = function () {
self.attempts = 1

if (self.options.heartbeatMsg && self.heartbeatInterval === null) {
self.missedHeartbeats = 0
self.heartbeatInterval = setInterval(function () {
self.sendHeartbeat()
}, self.options.heartbeatLapse)
}
if (typeof self.options.connected === 'function') {
self.options.connected()
}
}
ws.onmessage = function (evt) {
if (self.options.heartbeatMsg && evt.data === self.options.heartbeatMsg) {
self.missedHeartbeats = 0
} else if (typeof self.options.receiveMessage === 'function') {
return self.options.receiveMessage(evt.data, evt)
}
}
ws.onerror = function (evt) {
if (typeof self.options.error === 'function') {
self.options.error(evt)
}
}
ws.onclose = function (evt) {
if (self.heartbeatInterval) {
clearInterval(self.heartbeatInterval)
self.heartbeatInterval = null
}
if (typeof self.options.disconnected === 'function') {
self.options.disconnected(evt)
}
if (!self.forcedClose) {
self.forcedClose = false
self.reconnect()
}
}
self.ws = ws
self.timer = null
} catch (e) {
self.reconnect(e)
}
}

reconnect (e) {
const self = this
if (self.options.mustReconnect && !self.timer && self.attempts < self.options.maxAttempts) {
const time = generateInteval(self.attempts)
if (typeof self.options.reconnecting === 'function') {
self.options.reconnecting(self.attempts, time, e)
}
self.timer = setTimeout(function () {
self.attempts++
self.connect()
}, time)
}
}

close () {
const self = this
self.forcedClose = true
if (!self.isClosing() || !self.isClosed()) {
self.ws.close()
}
}

sendHeartbeat () {
const self = this
try {
self.missedHeartbeats++
if (self.missedHeartbeats >= self.options.maxMissedHeartbeats) { throw new Error('Too many missed heartbeats.') }
self.ws.send(self.options.heartbeatMsg)
} catch (e) {
if (typeof self.options.heartbeatTimeout === 'function') {
self.options.heartbeatTimeout(e)
}
if (!self.isClosing() && !self.isClosed()) {
self.ws.close()
}
}
}

sendMessage (message) {
this.ws.send(message)
}

getState () {
return this.ws.readyState
}

isConnecting () {
return this.ws && this.ws.readyState === 0
}

isConnected () {
return this.ws && this.ws.readyState === 1
}

isClosing () {
return this.ws && this.ws.readyState === 2
}

isClosed () {
return this.ws && this.ws.readyState === 3
}
}

export default WS4Redis