socketcluster-client
Version:
SocketCluster JavaScript client
318 lines (259 loc) • 7.86 kB
JavaScript
var WebSocket = require('sc-ws');
var SCEmitter = require('sc-emitter').SCEmitter;
var formatter = require('sc-formatter');
var Response = require('./response').Response;
var querystring = require('querystring');
var SCTransport = function (authEngine, options) {
this.state = this.CLOSED;
this.auth = authEngine;
this.options = options;
this.pingTimeout = options.ackTimeout;
this.callIdGenerator = options.callIdGenerator;
this._pingTimeoutTicker = null;
this._callbackMap = {};
this.open();
};
SCTransport.prototype = Object.create(SCEmitter.prototype);
SCTransport.CONNECTING = SCTransport.prototype.CONNECTING = 'connecting';
SCTransport.OPEN = SCTransport.prototype.OPEN = 'open';
SCTransport.CLOSED = SCTransport.prototype.CLOSED = 'closed';
SCTransport.prototype.uri = function () {
var query = this.options.query || {};
var schema = this.options.secure ? 'wss' : 'ws';
var port = '';
if (this.options.port && (('wss' == schema && this.options.port != 443)
|| ('ws' == schema && this.options.port != 80))) {
port = ':' + this.options.port;
}
if (this.options.timestampRequests) {
query[this.options.timestampParam] = (new Date()).getTime();
}
query = querystring.stringify(query);
if (query.length) {
query = '?' + query;
}
return schema + '://' + this.options.hostname + port + this.options.path + query;
};
SCTransport.prototype.open = function () {
var self = this;
this.state = this.CONNECTING;
var uri = this.uri();
var wsSocket = new WebSocket(uri, null, this.options);
wsSocket.binaryType = this.options.binaryType;
this.socket = wsSocket;
wsSocket.onopen = function () {
self._onOpen();
};
wsSocket.onclose = function (event) {
self._onClose(event.code, event.reason);
};
wsSocket.onmessage = function (message, flags) {
self._onMessage(message.data);
};
wsSocket.onerror = function (error) {
// The onclose event will be called automatically after the onerror event
// if the socket is connected - Otherwise, if it's in the middle of
// connecting, we want to close it manually with a 1006 - This is necessary
// to prevent inconsistent behavior when running the client in Node.js
// vs in a browser.
if (self.state === self.CONNECTING) {
self._onClose(1006);
}
};
};
SCTransport.prototype._onOpen = function () {
var self = this;
this._resetPingTimeout();
this._handshake(function (err, status) {
if (err) {
self._onError(err);
self._onClose(4003);
self.socket.close(4003);
} else {
self.state = self.OPEN;
SCEmitter.prototype.emit.call(self, 'open', status);
self._resetPingTimeout();
}
});
};
SCTransport.prototype._handshake = function (callback) {
var self = this;
this.auth.loadToken(this.options.authTokenName, function (err, token) {
if (err) {
callback(err);
} else {
// Don't wait for this.state to be 'open'.
// The underlying WebSocket (this.socket) is already open.
var options = {
force: true
};
self.emit('#handshake', {
authToken: token
}, options, callback);
}
});
};
SCTransport.prototype._onClose = function (code, data) {
delete this.socket.onopen;
delete this.socket.onclose;
delete this.socket.onmessage;
delete this.socket.onerror;
if (this.state == this.OPEN) {
this.state = this.CLOSED;
SCEmitter.prototype.emit.call(this, 'close', code, data);
} else if (this.state == this.CONNECTING) {
this.state = this.CLOSED;
SCEmitter.prototype.emit.call(this, 'openAbort', code, data);
}
};
SCTransport.prototype._onMessage = function (message) {
SCEmitter.prototype.emit.call(this, 'event', 'message', message);
// If ping
if (message == '1') {
this._resetPingTimeout();
if (this.socket.readyState == this.socket.OPEN) {
this.socket.send('2');
}
} else {
var obj;
try {
obj = this.parse(message);
} catch (err) {
obj = message;
}
var event = obj.event;
if (event) {
var response = new Response(this, obj.cid);
SCEmitter.prototype.emit.call(this, 'event', event, obj.data, response);
} else if (obj.rid != null) {
var eventObject = this._callbackMap[obj.rid];
if (eventObject) {
clearTimeout(eventObject.timeout);
delete this._callbackMap[obj.rid];
if (eventObject.callback) {
eventObject.callback(obj.error, obj.data);
}
}
if (obj.error) {
this._onError(obj.error);
}
} else {
SCEmitter.prototype.emit.call(this, 'event', 'raw', obj);
}
}
};
SCTransport.prototype._onError = function (err) {
SCEmitter.prototype.emit.call(this, 'error', err);
};
SCTransport.prototype._resetPingTimeout = function () {
var self = this;
var now = (new Date()).getTime();
clearTimeout(this._pingTimeoutTicker);
this._pingTimeoutTicker = setTimeout(function () {
self._onClose(4000);
self.socket.close(4000);
}, this.pingTimeout);
};
SCTransport.prototype.getBytesReceived = function () {
return this.socket.bytesReceived;
};
SCTransport.prototype.close = function (code, data) {
code = code || 1000;
if (this.state == this.OPEN) {
var packet = {
code: code,
data: data
};
this.emit('#disconnect', packet);
this._onClose(code, data);
this.socket.close(code);
} else if (this.state == this.CONNECTING) {
this._onClose(code, data);
this.socket.close(code);
}
};
SCTransport.prototype.emitRaw = function (eventObject) {
eventObject.cid = this.callIdGenerator();
if (eventObject.callback) {
this._callbackMap[eventObject.cid] = eventObject;
}
var simpleEventObject = {
event: eventObject.event,
data: eventObject.data,
cid: eventObject.cid
};
this.sendObject(simpleEventObject);
return eventObject.cid;
};
SCTransport.prototype._handleEventAckTimeout = function (eventObject) {
var errorMessage = "Event response for '" + eventObject.event + "' timed out";
var error = new Error(errorMessage);
error.type = 'timeout';
if (eventObject.cid) {
delete this._callbackMap[eventObject.cid];
}
var callback = eventObject.callback;
delete eventObject.callback;
callback.call(eventObject, error, eventObject);
this._onError(error);
};
// The last two optional arguments (a and b) can be options and/or callback
SCTransport.prototype.emit = function (event, data, a, b) {
var self = this;
var callback, options;
if (b) {
options = a;
callback = b;
} else {
if (a instanceof Function) {
options = {};
callback = a;
} else {
options = a;
}
}
var eventObject = {
event: event,
data: data,
callback: callback
};
if (callback && !options.noTimeout) {
eventObject.timeout = setTimeout(function () {
self._handleEventAckTimeout(eventObject);
}, this.options.ackTimeout);
}
var cid = null;
if (this.state == this.OPEN || options.force) {
cid = this.emitRaw(eventObject);
}
return cid;
};
SCTransport.prototype.cancelPendingResponse = function (cid) {
delete this._callbackMap[cid];
};
SCTransport.prototype.parse = function (message) {
return formatter.parse(message);
};
SCTransport.prototype.stringify = function (object) {
return formatter.stringify(object);
};
SCTransport.prototype.send = function (data) {
if (this.socket.readyState != this.socket.OPEN) {
this._onClose(1005);
} else {
this.socket.send(data);
}
};
SCTransport.prototype.sendObject = function (object) {
var str, formatError;
try {
str = this.stringify(object);
} catch (err) {
formatError = err;
this._onError(formatError);
}
if (!formatError) {
this.send(str);
}
};
module.exports.SCTransport = SCTransport;