@omneedia/socketcluster
Version:
SocketCluster - A Highly parallelized WebSocket server cluster to make the most of multi-core machines/instances.
1,692 lines (1,415 loc) • 156 kB
JavaScript
/**
* SocketCluster JavaScript client v14.2.2
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.socketCluster = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(_dereq_,module,exports){
var SCClientSocket = _dereq_('./lib/scclientsocket');
var factory = _dereq_('./lib/factory');
module.exports.factory = factory;
module.exports.SCClientSocket = SCClientSocket;
module.exports.Emitter = _dereq_('component-emitter');
module.exports.create = function (options) {
return factory.create(options);
};
module.exports.connect = module.exports.create;
module.exports.destroy = function (socket) {
return factory.destroy(socket);
};
module.exports.clients = factory.clients;
module.exports.version = '14.2.2';
},{"./lib/factory":3,"./lib/scclientsocket":5,"component-emitter":11}],2:[function(_dereq_,module,exports){
(function (global){
var AuthEngine = function () {
this._internalStorage = {};
this.isLocalStorageEnabled = this._checkLocalStorageEnabled();
};
AuthEngine.prototype._checkLocalStorageEnabled = function () {
var err;
try {
// Some browsers will throw an error here if localStorage is disabled.
global.localStorage;
// Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem
// throw QuotaExceededError. We're going to detect this and avoid hard to debug edge cases.
global.localStorage.setItem('__scLocalStorageTest', 1);
global.localStorage.removeItem('__scLocalStorageTest');
} catch (e) {
err = e;
}
return !err;
};
AuthEngine.prototype.saveToken = function (name, token, options, callback) {
if (this.isLocalStorageEnabled && global.localStorage) {
global.localStorage.setItem(name, token);
} else {
this._internalStorage[name] = token;
}
callback && callback(null, token);
};
AuthEngine.prototype.removeToken = function (name, callback) {
var token;
this.loadToken(name, function (err, authToken) {
token = authToken;
});
if (this.isLocalStorageEnabled && global.localStorage) {
global.localStorage.removeItem(name);
} else {
delete this._internalStorage[name];
}
callback && callback(null, token);
};
AuthEngine.prototype.loadToken = function (name, callback) {
var token;
if (this.isLocalStorageEnabled && global.localStorage) {
token = global.localStorage.getItem(name);
} else {
token = this._internalStorage[name] || null;
}
callback(null, token);
};
module.exports.AuthEngine = AuthEngine;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],3:[function(_dereq_,module,exports){
(function (global){
var SCClientSocket = _dereq_('./scclientsocket');
var scErrors = _dereq_('sc-errors');
var uuid = _dereq_('uuid');
var InvalidArgumentsError = scErrors.InvalidArgumentsError;
var _clients = {};
function getMultiplexId(options) {
var protocolPrefix = options.secure ? 'https://' : 'http://';
var queryString = '';
if (options.query) {
if (typeof options.query === 'string') {
queryString = options.query;
} else {
var queryArray = [];
var queryMap = options.query;
for (var key in queryMap) {
if (queryMap.hasOwnProperty(key)) {
queryArray.push(key + '=' + queryMap[key]);
}
}
if (queryArray.length) {
queryString = '?' + queryArray.join('&');
}
}
}
var host;
if (options.host) {
host = options.host;
} else {
host = options.hostname + ':' + options.port;
}
return protocolPrefix + host + options.path + queryString;
}
function isUrlSecure() {
return global.location && location.protocol === 'https:';
}
function getPort(options, isSecureDefault) {
var isSecure = options.secure == null ? isSecureDefault : options.secure;
return options.port || (global.location && location.port ? location.port : isSecure ? 443 : 80);
}
function create(options) {
var self = this;
options = options || {};
if (options.host && !options.host.match(/[^:]+:\d{2,5}/)) {
throw new InvalidArgumentsError('The host option should include both' +
' the hostname and the port number in the format "hostname:port"');
}
if (options.host && options.hostname) {
throw new InvalidArgumentsError('The host option should already include' +
' the hostname and the port number in the format "hostname:port"' +
' - Because of this, you should never use host and hostname options together');
}
if (options.host && options.port) {
throw new InvalidArgumentsError('The host option should already include' +
' the hostname and the port number in the format "hostname:port"' +
' - Because of this, you should never use host and port options together');
}
var isSecureDefault = isUrlSecure();
var opts = {
port: getPort(options, isSecureDefault),
hostname: global.location && location.hostname || 'localhost',
path: '/socketcluster/',
secure: isSecureDefault,
autoConnect: true,
autoReconnect: true,
autoSubscribeOnConnect: true,
connectTimeout: 20000,
ackTimeout: 10000,
timestampRequests: false,
timestampParam: 't',
authEngine: null,
authTokenName: 'socketCluster.authToken',
binaryType: 'arraybuffer',
multiplex: true,
pubSubBatchDuration: null,
cloneData: false
};
for (var i in options) {
if (options.hasOwnProperty(i)) {
opts[i] = options[i];
}
}
opts.clientMap = _clients;
if (opts.multiplex === false) {
opts.clientId = uuid.v4();
var socket = new SCClientSocket(opts);
_clients[opts.clientId] = socket;
return socket;
}
opts.clientId = getMultiplexId(opts);
if (_clients[opts.clientId]) {
if (opts.autoConnect) {
_clients[opts.clientId].connect();
}
} else {
_clients[opts.clientId] = new SCClientSocket(opts);
}
return _clients[opts.clientId];
}
function destroy(socket) {
socket.destroy();
}
module.exports = {
create: create,
destroy: destroy,
clients: _clients
};
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./scclientsocket":5,"sc-errors":20,"uuid":22}],4:[function(_dereq_,module,exports){
var scErrors = _dereq_('sc-errors');
var InvalidActionError = scErrors.InvalidActionError;
var Response = function (socket, id) {
this.socket = socket;
this.id = id;
this.sent = false;
};
Response.prototype._respond = function (responseData) {
if (this.sent) {
throw new InvalidActionError('Response ' + this.id + ' has already been sent');
} else {
this.sent = true;
this.socket.send(this.socket.encode(responseData));
}
};
Response.prototype.end = function (data) {
if (this.id) {
var responseData = {
rid: this.id
};
if (data !== undefined) {
responseData.data = data;
}
this._respond(responseData);
}
};
Response.prototype.error = function (error, data) {
if (this.id) {
var err = scErrors.dehydrateError(error);
var responseData = {
rid: this.id,
error: err
};
if (data !== undefined) {
responseData.data = data;
}
this._respond(responseData);
}
};
Response.prototype.callback = function (error, data) {
if (error) {
this.error(error, data);
} else {
this.end(data);
}
};
module.exports.Response = Response;
},{"sc-errors":20}],5:[function(_dereq_,module,exports){
(function (global){
var Emitter = _dereq_('component-emitter');
var SCChannel = _dereq_('sc-channel').SCChannel;
var Response = _dereq_('./response').Response;
var AuthEngine = _dereq_('./auth').AuthEngine;
var formatter = _dereq_('sc-formatter');
var SCTransport = _dereq_('./sctransport').SCTransport;
var querystring = _dereq_('querystring');
var LinkedList = _dereq_('linked-list');
var Buffer = _dereq_('buffer/').Buffer;
var clone = _dereq_('clone');
var scErrors = _dereq_('sc-errors');
var InvalidArgumentsError = scErrors.InvalidArgumentsError;
var InvalidMessageError = scErrors.InvalidMessageError;
var InvalidActionError = scErrors.InvalidActionError;
var SocketProtocolError = scErrors.SocketProtocolError;
var TimeoutError = scErrors.TimeoutError;
var BadConnectionError = scErrors.BadConnectionError;
var isBrowser = typeof window !== 'undefined';
var SCClientSocket = function (opts) {
var self = this;
Emitter.call(this);
this.id = null;
this.state = this.CLOSED;
this.authState = this.UNAUTHENTICATED;
this.signedAuthToken = null;
this.authToken = null;
this.pendingReconnect = false;
this.pendingReconnectTimeout = null;
this.preparingPendingSubscriptions = false;
this.clientId = opts.clientId;
this.connectTimeout = opts.connectTimeout;
this.ackTimeout = opts.ackTimeout;
this.channelPrefix = opts.channelPrefix || null;
this.disconnectOnUnload = opts.disconnectOnUnload == null ? true : opts.disconnectOnUnload;
this.authTokenName = opts.authTokenName;
// pingTimeout will be ackTimeout at the start, but it will
// be updated with values provided by the 'connect' event
this.pingTimeout = this.ackTimeout;
this.pingTimeoutDisabled = !!opts.pingTimeoutDisabled;
this.active = true;
this._clientMap = opts.clientMap || {};
var maxTimeout = Math.pow(2, 31) - 1;
var verifyDuration = function (propertyName) {
if (self[propertyName] > maxTimeout) {
throw new InvalidArgumentsError('The ' + propertyName +
' value provided exceeded the maximum amount allowed');
}
};
verifyDuration('connectTimeout');
verifyDuration('ackTimeout');
this._localEvents = {
'connect': 1,
'connectAbort': 1,
'close': 1,
'disconnect': 1,
'message': 1,
'error': 1,
'raw': 1,
'kickOut': 1,
'subscribe': 1,
'unsubscribe': 1,
'subscribeStateChange': 1,
'authStateChange': 1,
'authenticate': 1,
'deauthenticate': 1,
'removeAuthToken': 1,
'subscribeRequest': 1
};
this.connectAttempts = 0;
this._emitBuffer = new LinkedList();
this.channels = {};
this.options = opts;
this._cid = 1;
this.options.callIdGenerator = function () {
return self._cid++;
};
if (this.options.autoReconnect) {
if (this.options.autoReconnectOptions == null) {
this.options.autoReconnectOptions = {};
}
// Add properties to the this.options.autoReconnectOptions object.
// We assign the reference to a reconnectOptions variable to avoid repetition.
var reconnectOptions = this.options.autoReconnectOptions;
if (reconnectOptions.initialDelay == null) {
reconnectOptions.initialDelay = 10000;
}
if (reconnectOptions.randomness == null) {
reconnectOptions.randomness = 10000;
}
if (reconnectOptions.multiplier == null) {
reconnectOptions.multiplier = 1.5;
}
if (reconnectOptions.maxDelay == null) {
reconnectOptions.maxDelay = 60000;
}
}
if (this.options.subscriptionRetryOptions == null) {
this.options.subscriptionRetryOptions = {};
}
if (this.options.authEngine) {
this.auth = this.options.authEngine;
} else {
this.auth = new AuthEngine();
}
if (this.options.codecEngine) {
this.codec = this.options.codecEngine;
} else {
// Default codec engine
this.codec = formatter;
}
if (this.options.protocol) {
var protocolOptionError = new InvalidArgumentsError('The "protocol" option' +
' does not affect socketcluster-client. If you want to utilize SSL/TLS' +
' - use "secure" option instead');
this._onSCError(protocolOptionError);
}
this.options.path = this.options.path.replace(/\/$/, '') + '/';
this.options.query = opts.query || {};
if (typeof this.options.query === 'string') {
this.options.query = querystring.parse(this.options.query);
}
this._channelEmitter = new Emitter();
this._unloadHandler = function () {
self.disconnect();
};
if (isBrowser && this.disconnectOnUnload && global.addEventListener) {
global.addEventListener('beforeunload', this._unloadHandler, false);
}
this._clientMap[this.clientId] = this;
if (this.options.autoConnect) {
this.connect();
}
};
SCClientSocket.prototype = Object.create(Emitter.prototype);
SCClientSocket.CONNECTING = SCClientSocket.prototype.CONNECTING = SCTransport.prototype.CONNECTING;
SCClientSocket.OPEN = SCClientSocket.prototype.OPEN = SCTransport.prototype.OPEN;
SCClientSocket.CLOSED = SCClientSocket.prototype.CLOSED = SCTransport.prototype.CLOSED;
SCClientSocket.AUTHENTICATED = SCClientSocket.prototype.AUTHENTICATED = 'authenticated';
SCClientSocket.UNAUTHENTICATED = SCClientSocket.prototype.UNAUTHENTICATED = 'unauthenticated';
SCClientSocket.PENDING = SCClientSocket.prototype.PENDING = 'pending';
SCClientSocket.ignoreStatuses = scErrors.socketProtocolIgnoreStatuses;
SCClientSocket.errorStatuses = scErrors.socketProtocolErrorStatuses;
SCClientSocket.prototype._privateEventHandlerMap = {
'#publish': function (data) {
var undecoratedChannelName = this._undecorateChannelName(data.channel);
var isSubscribed = this.isSubscribed(undecoratedChannelName, true);
if (isSubscribed) {
this._channelEmitter.emit(undecoratedChannelName, data.data);
}
},
'#kickOut': function (data) {
var undecoratedChannelName = this._undecorateChannelName(data.channel);
var channel = this.channels[undecoratedChannelName];
if (channel) {
Emitter.prototype.emit.call(this, 'kickOut', data.message, undecoratedChannelName);
channel.emit('kickOut', data.message, undecoratedChannelName);
this._triggerChannelUnsubscribe(channel);
}
},
'#setAuthToken': function (data, response) {
var self = this;
if (data) {
var triggerAuthenticate = function (err) {
if (err) {
// This is a non-fatal error, we don't want to close the connection
// because of this but we do want to notify the server and throw an error
// on the client.
response.error(err);
self._onSCError(err);
} else {
self._changeToAuthenticatedState(data.token);
response.end();
}
};
this.auth.saveToken(this.authTokenName, data.token, {}, triggerAuthenticate);
} else {
response.error(new InvalidMessageError('No token data provided by #setAuthToken event'));
}
},
'#removeAuthToken': function (data, response) {
var self = this;
this.auth.removeToken(this.authTokenName, function (err, oldToken) {
if (err) {
// Non-fatal error - Do not close the connection
response.error(err);
self._onSCError(err);
} else {
Emitter.prototype.emit.call(self, 'removeAuthToken', oldToken);
self._changeToUnauthenticatedStateAndClearTokens();
response.end();
}
});
},
'#disconnect': function (data) {
this.transport.close(data.code, data.data);
}
};
SCClientSocket.prototype.getState = function () {
return this.state;
};
SCClientSocket.prototype.getBytesReceived = function () {
return this.transport.getBytesReceived();
};
SCClientSocket.prototype.deauthenticate = function (callback) {
var self = this;
this.auth.removeToken(this.authTokenName, function (err, oldToken) {
if (err) {
// Non-fatal error - Do not close the connection
self._onSCError(err);
} else {
Emitter.prototype.emit.call(self, 'removeAuthToken', oldToken);
if (self.state !== self.CLOSED) {
self.emit('#removeAuthToken');
}
self._changeToUnauthenticatedStateAndClearTokens();
}
callback && callback(err);
});
};
SCClientSocket.prototype.connect = SCClientSocket.prototype.open = function () {
var self = this;
if (!this.active) {
var error = new InvalidActionError('Cannot connect a destroyed client');
this._onSCError(error);
return;
}
if (this.state === this.CLOSED) {
this.pendingReconnect = false;
this.pendingReconnectTimeout = null;
clearTimeout(this._reconnectTimeoutRef);
this.state = this.CONNECTING;
Emitter.prototype.emit.call(this, 'connecting');
if (this.transport) {
this.transport.off();
}
this.transport = new SCTransport(this.auth, this.codec, this.options);
this.transport.on('open', function (status) {
self.state = self.OPEN;
self._onSCOpen(status);
});
this.transport.on('error', function (err) {
self._onSCError(err);
});
this.transport.on('close', function (code, data) {
self.state = self.CLOSED;
self._onSCClose(code, data);
});
this.transport.on('openAbort', function (code, data) {
self.state = self.CLOSED;
self._onSCClose(code, data, true);
});
this.transport.on('event', function (event, data, res) {
self._onSCEvent(event, data, res);
});
}
};
SCClientSocket.prototype.reconnect = function (code, data) {
this.disconnect(code, data);
this.connect();
};
SCClientSocket.prototype.disconnect = function (code, data) {
code = code || 1000;
if (typeof code !== 'number') {
throw new InvalidArgumentsError('If specified, the code argument must be a number');
}
if (this.state === this.OPEN || this.state === this.CONNECTING) {
this.transport.close(code, data);
} else {
this.pendingReconnect = false;
this.pendingReconnectTimeout = null;
clearTimeout(this._reconnectTimeoutRef);
}
};
SCClientSocket.prototype.destroy = function (code, data) {
if (isBrowser && global.removeEventListener) {
global.removeEventListener('beforeunload', this._unloadHandler, false);
}
this.active = false;
this.disconnect(code, data);
delete this._clientMap[this.clientId];
};
SCClientSocket.prototype._changeToUnauthenticatedStateAndClearTokens = function () {
if (this.authState !== this.UNAUTHENTICATED) {
var oldState = this.authState;
var oldSignedToken = this.signedAuthToken;
this.authState = this.UNAUTHENTICATED;
this.signedAuthToken = null;
this.authToken = null;
var stateChangeData = {
oldState: oldState,
newState: this.authState
};
Emitter.prototype.emit.call(this, 'authStateChange', stateChangeData);
Emitter.prototype.emit.call(this, 'deauthenticate', oldSignedToken);
}
};
SCClientSocket.prototype._changeToAuthenticatedState = function (signedAuthToken) {
this.signedAuthToken = signedAuthToken;
this.authToken = this._extractAuthTokenData(signedAuthToken);
if (this.authState !== this.AUTHENTICATED) {
var oldState = this.authState;
this.authState = this.AUTHENTICATED;
var stateChangeData = {
oldState: oldState,
newState: this.authState,
signedAuthToken: signedAuthToken,
authToken: this.authToken
};
if (!this.preparingPendingSubscriptions) {
this.processPendingSubscriptions();
}
Emitter.prototype.emit.call(this, 'authStateChange', stateChangeData);
}
Emitter.prototype.emit.call(this, 'authenticate', signedAuthToken);
};
SCClientSocket.prototype.decodeBase64 = function (encodedString) {
return Buffer.from(encodedString, 'base64').toString('utf8');
};
SCClientSocket.prototype.encodeBase64 = function (decodedString) {
return Buffer.from(decodedString, 'utf8').toString('base64');
};
SCClientSocket.prototype._extractAuthTokenData = function (signedAuthToken) {
var tokenParts = (signedAuthToken || '').split('.');
var encodedTokenData = tokenParts[1];
if (encodedTokenData != null) {
var tokenData = encodedTokenData;
try {
tokenData = this.decodeBase64(tokenData);
return JSON.parse(tokenData);
} catch (e) {
return tokenData;
}
}
return null;
};
SCClientSocket.prototype.getAuthToken = function () {
return this.authToken;
};
SCClientSocket.prototype.getSignedAuthToken = function () {
return this.signedAuthToken;
};
// Perform client-initiated authentication by providing an encrypted token string.
SCClientSocket.prototype.authenticate = function (signedAuthToken, callback) {
var self = this;
this.emit('#authenticate', signedAuthToken, function (err, authStatus) {
if (authStatus && authStatus.isAuthenticated != null) {
// If authStatus is correctly formatted (has an isAuthenticated property),
// then we will rehydrate the authError.
if (authStatus.authError) {
authStatus.authError = scErrors.hydrateError(authStatus.authError);
}
} else {
// Some errors like BadConnectionError and TimeoutError will not pass a valid
// authStatus object to the current function, so we need to create it ourselves.
authStatus = {
isAuthenticated: self.authState,
authError: null
};
}
if (err) {
if (err.name !== 'BadConnectionError' && err.name !== 'TimeoutError') {
// In case of a bad/closed connection or a timeout, we maintain the last
// known auth state since those errors don't mean that the token is invalid.
self._changeToUnauthenticatedStateAndClearTokens();
}
callback && callback(err, authStatus);
} else {
self.auth.saveToken(self.authTokenName, signedAuthToken, {}, function (err) {
if (err) {
self._onSCError(err);
}
if (authStatus.isAuthenticated) {
self._changeToAuthenticatedState(signedAuthToken);
} else {
self._changeToUnauthenticatedStateAndClearTokens();
}
callback && callback(err, authStatus);
});
}
});
};
SCClientSocket.prototype._tryReconnect = function (initialDelay) {
var self = this;
var exponent = this.connectAttempts++;
var reconnectOptions = this.options.autoReconnectOptions;
var timeout;
if (initialDelay == null || exponent > 0) {
var initialTimeout = Math.round(reconnectOptions.initialDelay + (reconnectOptions.randomness || 0) * Math.random());
timeout = Math.round(initialTimeout * Math.pow(reconnectOptions.multiplier, exponent));
} else {
timeout = initialDelay;
}
if (timeout > reconnectOptions.maxDelay) {
timeout = reconnectOptions.maxDelay;
}
clearTimeout(this._reconnectTimeoutRef);
this.pendingReconnect = true;
this.pendingReconnectTimeout = timeout;
this._reconnectTimeoutRef = setTimeout(function () {
self.connect();
}, timeout);
};
SCClientSocket.prototype._onSCOpen = function (status) {
var self = this;
this.preparingPendingSubscriptions = true;
if (status) {
this.id = status.id;
this.pingTimeout = status.pingTimeout;
this.transport.pingTimeout = this.pingTimeout;
if (status.isAuthenticated) {
this._changeToAuthenticatedState(status.authToken);
} else {
this._changeToUnauthenticatedStateAndClearTokens();
}
} else {
// This can happen if auth.loadToken (in sctransport.js) fails with
// an error - This means that the signedAuthToken cannot be loaded by
// the auth engine and therefore, we need to unauthenticate the client.
this._changeToUnauthenticatedStateAndClearTokens();
}
this.connectAttempts = 0;
if (this.options.autoSubscribeOnConnect) {
this.processPendingSubscriptions();
}
// If the user invokes the callback while in autoSubscribeOnConnect mode, it
// won't break anything.
Emitter.prototype.emit.call(this, 'connect', status, function () {
self.processPendingSubscriptions();
});
if (this.state === this.OPEN) {
this._flushEmitBuffer();
}
};
SCClientSocket.prototype._onSCError = function (err) {
var self = this;
// Throw error in different stack frame so that error handling
// cannot interfere with a reconnect action.
setTimeout(function () {
if (self.listeners('error').length < 1) {
throw err;
} else {
Emitter.prototype.emit.call(self, 'error', err);
}
}, 0);
};
SCClientSocket.prototype._suspendSubscriptions = function () {
var channel, newState;
for (var channelName in this.channels) {
if (this.channels.hasOwnProperty(channelName)) {
channel = this.channels[channelName];
if (channel.state === channel.SUBSCRIBED ||
channel.state === channel.PENDING) {
newState = channel.PENDING;
} else {
newState = channel.UNSUBSCRIBED;
}
this._triggerChannelUnsubscribe(channel, newState);
}
}
};
SCClientSocket.prototype._abortAllPendingEventsDueToBadConnection = function (failureType) {
var currentNode = this._emitBuffer.head;
var nextNode;
while (currentNode) {
nextNode = currentNode.next;
var eventObject = currentNode.data;
clearTimeout(eventObject.timeout);
delete eventObject.timeout;
currentNode.detach();
currentNode = nextNode;
var callback = eventObject.callback;
if (callback) {
delete eventObject.callback;
var errorMessage = "Event '" + eventObject.event +
"' was aborted due to a bad connection";
var error = new BadConnectionError(errorMessage, failureType);
callback.call(eventObject, error, eventObject);
}
// Cleanup any pending response callback in the transport layer too.
if (eventObject.cid) {
this.transport.cancelPendingResponse(eventObject.cid);
}
}
};
SCClientSocket.prototype._onSCClose = function (code, data, openAbort) {
var self = this;
this.id = null;
if (this.transport) {
this.transport.off();
}
this.pendingReconnect = false;
this.pendingReconnectTimeout = null;
clearTimeout(this._reconnectTimeoutRef);
this._suspendSubscriptions();
this._abortAllPendingEventsDueToBadConnection(openAbort ? 'connectAbort' : 'disconnect');
// Try to reconnect
// on server ping timeout (4000)
// or on client pong timeout (4001)
// or on close without status (1005)
// or on handshake failure (4003)
// or on handshake rejection (4008)
// or on socket hung up (1006)
if (this.options.autoReconnect) {
if (code === 4000 || code === 4001 || code === 1005) {
// If there is a ping or pong timeout or socket closes without
// status, don't wait before trying to reconnect - These could happen
// if the client wakes up after a period of inactivity and in this case we
// want to re-establish the connection as soon as possible.
this._tryReconnect(0);
// Codes 4500 and above will be treated as permanent disconnects.
// Socket will not try to auto-reconnect.
} else if (code !== 1000 && code < 4500) {
this._tryReconnect();
}
}
if (openAbort) {
Emitter.prototype.emit.call(self, 'connectAbort', code, data);
} else {
Emitter.prototype.emit.call(self, 'disconnect', code, data);
}
Emitter.prototype.emit.call(self, 'close', code, data);
if (!SCClientSocket.ignoreStatuses[code]) {
var closeMessage;
if (data) {
closeMessage = 'Socket connection closed with status code ' + code + ' and reason: ' + data;
} else {
closeMessage = 'Socket connection closed with status code ' + code;
}
var err = new SocketProtocolError(SCClientSocket.errorStatuses[code] || closeMessage, code);
this._onSCError(err);
}
};
SCClientSocket.prototype._onSCEvent = function (event, data, res) {
var handler = this._privateEventHandlerMap[event];
if (handler) {
handler.call(this, data, res);
} else {
Emitter.prototype.emit.call(this, event, data, function () {
res && res.callback.apply(res, arguments);
});
}
};
SCClientSocket.prototype.decode = function (message) {
return this.transport.decode(message);
};
SCClientSocket.prototype.encode = function (object) {
return this.transport.encode(object);
};
SCClientSocket.prototype._flushEmitBuffer = function () {
var currentNode = this._emitBuffer.head;
var nextNode;
while (currentNode) {
nextNode = currentNode.next;
var eventObject = currentNode.data;
currentNode.detach();
this.transport.emitObject(eventObject);
currentNode = nextNode;
}
};
SCClientSocket.prototype._handleEventAckTimeout = function (eventObject, eventNode) {
if (eventNode) {
eventNode.detach();
}
delete eventObject.timeout;
var callback = eventObject.callback;
if (callback) {
delete eventObject.callback;
var error = new TimeoutError("Event response for '" + eventObject.event + "' timed out");
callback.call(eventObject, error, eventObject);
}
// Cleanup any pending response callback in the transport layer too.
if (eventObject.cid) {
this.transport.cancelPendingResponse(eventObject.cid);
}
};
SCClientSocket.prototype._emit = function (event, data, callback) {
var self = this;
if (this.state === this.CLOSED) {
this.connect();
}
var eventObject = {
event: event,
callback: callback
};
var eventNode = new LinkedList.Item();
if (this.options.cloneData) {
eventObject.data = clone(data);
} else {
eventObject.data = data;
}
eventNode.data = eventObject;
eventObject.timeout = setTimeout(function () {
self._handleEventAckTimeout(eventObject, eventNode);
}, this.ackTimeout);
this._emitBuffer.append(eventNode);
if (this.state === this.OPEN) {
this._flushEmitBuffer();
}
};
SCClientSocket.prototype.send = function (data) {
this.transport.send(data);
};
SCClientSocket.prototype.emit = function (event, data, callback) {
if (this._localEvents[event] == null) {
this._emit(event, data, callback);
} else if (event === 'error') {
Emitter.prototype.emit.call(this, event, data);
} else {
var error = new InvalidActionError('The "' + event + '" event is reserved and cannot be emitted on a client socket');
this._onSCError(error);
}
};
SCClientSocket.prototype.publish = function (channelName, data, callback) {
var pubData = {
channel: this._decorateChannelName(channelName),
data: data
};
this.emit('#publish', pubData, callback);
};
SCClientSocket.prototype._triggerChannelSubscribe = function (channel, subscriptionOptions) {
var channelName = channel.name;
if (channel.state !== channel.SUBSCRIBED) {
var oldState = channel.state;
channel.state = channel.SUBSCRIBED;
var stateChangeData = {
channel: channelName,
oldState: oldState,
newState: channel.state,
subscriptionOptions: subscriptionOptions
};
channel.emit('subscribeStateChange', stateChangeData);
channel.emit('subscribe', channelName, subscriptionOptions);
Emitter.prototype.emit.call(this, 'subscribeStateChange', stateChangeData);
Emitter.prototype.emit.call(this, 'subscribe', channelName, subscriptionOptions);
}
};
SCClientSocket.prototype._triggerChannelSubscribeFail = function (err, channel, subscriptionOptions) {
var channelName = channel.name;
var meetsAuthRequirements = !channel.waitForAuth || this.authState === this.AUTHENTICATED;
if (channel.state !== channel.UNSUBSCRIBED && meetsAuthRequirements) {
channel.state = channel.UNSUBSCRIBED;
channel.emit('subscribeFail', err, channelName, subscriptionOptions);
Emitter.prototype.emit.call(this, 'subscribeFail', err, channelName, subscriptionOptions);
}
};
// Cancel any pending subscribe callback
SCClientSocket.prototype._cancelPendingSubscribeCallback = function (channel) {
if (channel._pendingSubscriptionCid != null) {
this.transport.cancelPendingResponse(channel._pendingSubscriptionCid);
delete channel._pendingSubscriptionCid;
}
};
SCClientSocket.prototype._decorateChannelName = function (channelName) {
if (this.channelPrefix) {
channelName = this.channelPrefix + channelName;
}
return channelName;
};
SCClientSocket.prototype._undecorateChannelName = function (decoratedChannelName) {
if (this.channelPrefix && decoratedChannelName.indexOf(this.channelPrefix) === 0) {
return decoratedChannelName.replace(this.channelPrefix, '');
}
return decoratedChannelName;
};
SCClientSocket.prototype._trySubscribe = function (channel) {
var self = this;
var meetsAuthRequirements = !channel.waitForAuth || this.authState === this.AUTHENTICATED;
// We can only ever have one pending subscribe action at any given time on a channel
if (this.state === this.OPEN && !this.preparingPendingSubscriptions &&
channel._pendingSubscriptionCid == null && meetsAuthRequirements) {
var options = {
noTimeout: true
};
var subscriptionOptions = {
channel: this._decorateChannelName(channel.name)
};
if (channel.waitForAuth) {
options.waitForAuth = true;
subscriptionOptions.waitForAuth = options.waitForAuth;
}
if (channel.data) {
subscriptionOptions.data = channel.data;
}
if (channel.batch) {
options.batch = true;
subscriptionOptions.batch = true;
}
channel._pendingSubscriptionCid = this.transport.emit(
'#subscribe', subscriptionOptions, options,
function (err) {
delete channel._pendingSubscriptionCid;
if (err) {
self._triggerChannelSubscribeFail(err, channel, subscriptionOptions);
} else {
self._triggerChannelSubscribe(channel, subscriptionOptions);
}
}
);
Emitter.prototype.emit.call(this, 'subscribeRequest', channel.name, subscriptionOptions);
}
};
SCClientSocket.prototype.subscribe = function (channelName, options) {
var channel = this.channels[channelName];
if (!channel) {
channel = new SCChannel(channelName, this, options);
this.channels[channelName] = channel;
} else if (options) {
channel.setOptions(options);
}
if (channel.state === channel.UNSUBSCRIBED) {
channel.state = channel.PENDING;
this._trySubscribe(channel);
}
return channel;
};
SCClientSocket.prototype._triggerChannelUnsubscribe = function (channel, newState) {
var channelName = channel.name;
var oldState = channel.state;
if (newState) {
channel.state = newState;
} else {
channel.state = channel.UNSUBSCRIBED;
}
this._cancelPendingSubscribeCallback(channel);
if (oldState === channel.SUBSCRIBED) {
var stateChangeData = {
channel: channelName,
oldState: oldState,
newState: channel.state
};
channel.emit('subscribeStateChange', stateChangeData);
channel.emit('unsubscribe', channelName);
Emitter.prototype.emit.call(this, 'subscribeStateChange', stateChangeData);
Emitter.prototype.emit.call(this, 'unsubscribe', channelName);
}
};
SCClientSocket.prototype._tryUnsubscribe = function (channel) {
var self = this;
if (this.state === this.OPEN) {
var options = {
noTimeout: true
};
if (channel.batch) {
options.batch = true;
}
// If there is a pending subscribe action, cancel the callback
this._cancelPendingSubscribeCallback(channel);
// This operation cannot fail because the TCP protocol guarantees delivery
// so long as the connection remains open. If the connection closes,
// the server will automatically unsubscribe the client and thus complete
// the operation on the server side.
var decoratedChannelName = this._decorateChannelName(channel.name);
this.transport.emit('#unsubscribe', decoratedChannelName, options);
}
};
SCClientSocket.prototype.unsubscribe = function (channelName) {
var channel = this.channels[channelName];
if (channel) {
if (channel.state !== channel.UNSUBSCRIBED) {
this._triggerChannelUnsubscribe(channel);
this._tryUnsubscribe(channel);
}
}
};
SCClientSocket.prototype.channel = function (channelName, options) {
var currentChannel = this.channels[channelName];
if (!currentChannel) {
currentChannel = new SCChannel(channelName, this, options);
this.channels[channelName] = currentChannel;
}
return currentChannel;
};
SCClientSocket.prototype.destroyChannel = function (channelName) {
var channel = this.channels[channelName];
if (channel) {
channel.unwatch();
channel.unsubscribe();
delete this.channels[channelName];
}
};
SCClientSocket.prototype.subscriptions = function (includePending) {
var subs = [];
var channel, includeChannel;
for (var channelName in this.channels) {
if (this.channels.hasOwnProperty(channelName)) {
channel = this.channels[channelName];
if (includePending) {
includeChannel = channel && (channel.state === channel.SUBSCRIBED ||
channel.state === channel.PENDING);
} else {
includeChannel = channel && channel.state === channel.SUBSCRIBED;
}
if (includeChannel) {
subs.push(channelName);
}
}
}
return subs;
};
SCClientSocket.prototype.isSubscribed = function (channelName, includePending) {
var channel = this.channels[channelName];
if (includePending) {
return !!channel && (channel.state === channel.SUBSCRIBED ||
channel.state === channel.PENDING);
}
return !!channel && channel.state === channel.SUBSCRIBED;
};
SCClientSocket.prototype.processPendingSubscriptions = function () {
var self = this;
this.preparingPendingSubscriptions = false;
var pendingChannels = [];
for (var i in this.channels) {
if (this.channels.hasOwnProperty(i)) {
var channel = this.channels[i];
if (channel.state === channel.PENDING) {
pendingChannels.push(channel);
}
}
}
pendingChannels.sort(function (a, b) {
var ap = a.priority || 0;
var bp = b.priority || 0;
if (ap > bp) {
return -1;
}
if (ap < bp) {
return 1;
}
return 0;
});
pendingChannels.forEach(function (channel) {
self._trySubscribe(channel);
});
};
SCClientSocket.prototype.watch = function (channelName, handler) {
if (typeof handler !== 'function') {
throw new InvalidArgumentsError('No handler function was provided');
}
this._channelEmitter.on(channelName, handler);
};
SCClientSocket.prototype.unwatch = function (channelName, handler) {
if (handler) {
this._channelEmitter.removeListener(channelName, handler);
} else {
this._channelEmitter.removeAllListeners(channelName);
}
};
SCClientSocket.prototype.watchers = function (channelName) {
return this._channelEmitter.listeners(channelName);
};
module.exports = SCClientSocket;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./auth":2,"./response":4,"./sctransport":6,"buffer/":9,"clone":10,"component-emitter":11,"linked-list":14,"querystring":17,"sc-channel":18,"sc-errors":20,"sc-formatter":21}],6:[function(_dereq_,module,exports){
(function (global){
var Emitter = _dereq_('component-emitter');
var Response = _dereq_('./response').Response;
var querystring = _dereq_('querystring');
var WebSocket;
var createWebSocket;
if (global.WebSocket) {
WebSocket = global.WebSocket;
createWebSocket = function (uri, options) {
return new WebSocket(uri);
};
} else {
WebSocket = _dereq_('ws');
createWebSocket = function (uri, options) {
return new WebSocket(uri, null, options);
};
}
var scErrors = _dereq_('sc-errors');
var TimeoutError = scErrors.TimeoutError;
var BadConnectionError = scErrors.BadConnectionError;
var SCTransport = function (authEngine, codecEngine, options) {
var self = this;
this.state = this.CLOSED;
this.auth = authEngine;
this.codec = codecEngine;
this.options = options;
this.connectTimeout = options.connectTimeout;
this.pingTimeout = options.ackTimeout;
this.pingTimeoutDisabled = !!options.pingTimeoutDisabled;
this.callIdGenerator = options.callIdGenerator;
this.authTokenName = options.authTokenName;
this._pingTimeoutTicker = null;
this._callbackMap = {};
this._batchSendList = [];
// Open the connection.
this.state = this.CONNECTING;
var uri = this.uri();
var wsSocket = createWebSocket(uri, this.options);
wsSocket.binaryType = this.options.binaryType;
this.socket = wsSocket;
wsSocket.onopen = function () {
self._onOpen();
};
wsSocket.onclose = function (event) {
var code;
if (event.code == null) {
// This is to handle an edge case in React Native whereby
// event.code is undefined when the mobile device is locked.
// TODO: This is not perfect since this condition could also apply to
// an abnormal close (no close control frame) which would be a 1006.
code = 1005;
} else {
code = event.code;
}
self._onClose(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);
}
};
this._connectTimeoutRef = setTimeout(function () {
self._onClose(4007);
self.socket.close(4007);
}, this.connectTimeout);
};
SCTransport.prototype = Object.create(Emitter.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';
if (this.options.timestampRequests) {
query[this.options.timestampParam] = (new Date()).getTime();
}
query = querystring.encode(query);
if (query.length) {
query = '?' + query;
}
var host;
if (this.options.host) {
host = this.options.host;
} else {
var port = '';
if (this.options.port && ((schema === 'wss' && this.options.port !== 443)
|| (schema === 'ws' && this.options.port !== 80))) {
port = ':' + this.options.port;
}
host = this.options.hostname + port;
}
return schema + '://' + host + this.options.path + query;
};
SCTransport.prototype._onOpen = function () {
var self = this;
clearTimeout(this._connectTimeoutRef);
this._resetPingTimeout();
this._handshake(function (err, status) {
if (err) {
var statusCode;
if (status && status.code) {
statusCode = status.code;
} else {
statusCode = 4003;
}
self._onError(err);
self._onClose(statusCode, err.toString());
self.socket.close(statusCode);
} else {
self.state = self.OPEN;
Emitter.prototype.emit.call(self, 'open', status);
self._resetPingTimeout();
}
});
};
SCTransport.prototype._handshake = function (callback) {
var self = this;
this.auth.loadToken(this.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, function (err, status) {
if (status) {
// Add the token which was used as part of authentication attempt
// to the status object.
status.authToken = token;
if (status.authError) {
status.authError = scErrors.hydrateError(status.authError);
}
}
callback(err, status);
});
}
});
};
SCTransport.prototype._abortAllPendingEventsDueToBadConnection = function (failureType) {
for (var i in this._callbackMap) {
if (this._callbackMap.hasOwnProperty(i)) {
var eventObject = this._callbackMap[i];
delete this._callbackMap[i];
clearTimeout(eventObject.timeout);
delete eventObject.timeout;
var errorMessage = "Event '" + eventObject.event +
"' was aborted due to a bad connection";
var badConnectionError = new BadConnectionError(errorMessage, failureType);
var callback = eventObject.callback;
delete eventObject.callback;
callback.call(eventObject, badConnectionError, eventObject);
}
}
};
SCTransport.prototype._onClose = function (code, data) {
delete this.socket.onopen;
delete this.socket.onclose;
delete this.socket.onmessage;
delete this.socket.onerror;
clearTimeout(this._connectTimeoutRef);
clearTimeout(this._pingTimeoutTicker);
clearTimeout(this._batchTimeout);
if (this.state === this.OPEN) {
this.state = this.CLOSED;
Emitter.prototype.emit.call(this, 'close', code, data);
this._abortAllPendingEventsDueToBadConnection('disconnect');
} else if (this.state === this.CONNECTING) {
this.state = this.CLOSED;
Emitter.prototype.emit.call(this, 'openAbort', code, data);
this._abortAllPendingEventsDueToBadConnection('connectAbort');
}
};
SCTransport.prototype._handleEventObject = function (obj, message) {
if (obj && obj.event != null) {
var response = new Response(this, obj.cid);
Emitter.prototype.emit.call(this, 'event', obj.event, obj.data, response);
} else if (obj && obj.rid != null) {
var eventObject = this._callbackMap[obj.rid];
if (eventObject) {
clearTimeout(eventObject.timeout);
delete eventObject.timeout;
delete this._callbackMap[obj.rid];
if (eventObject.callback) {
var rehydratedError = scErrors.hydrateError(obj.error);
eventObject.callback(rehydratedError, obj.data);
}
}
} else {
Emitter.prototype.emit.call(this, 'event', 'raw', message);
}
};
SCTransport.prototype._onMessage = function (message) {
Emitter.prototype.emit.call(this, 'event', 'message', message);
var obj = this.decode(message);
// If ping
if (obj === '#1') {
this._resetPingTimeout();
if (this.socket.readyState === this.socket.OPEN) {
this.sendObject('#2');
}
} else {
if (Array.isArray(obj)) {
var len = obj.length;
for (var i = 0; i < len; i++) {
this._handleEventObject(obj[i], message);
}
} else {
this._handleEventObject(obj, message);
}
}
};
SCTransport.prototype._onError = function (err) {
Emitter.prototype.emit.call(this, 'error', err);
};
SCTransport.prototype._resetPingTimeout = function () {
if (this.pingTimeoutDisabled) {
return;
}
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.emitObject = function (eventObject, options) {
var simpleEventObject = {
event: eventObject.event,
data: eventObject.data
};
if (eventObject.callback) {
simpleEventObject.cid = eventObject.cid = this.callIdGenerator();
this._callbackMap[eventObject.cid] = eventObject;
}
this.sendObject(simpleEventObject, options);
return eventObject.cid || null;
};
SCTransport.prototype._handleEventAckTimeout = function (eventObject) {
if (eventObject.cid) {
delete this._callbackMap[eventObject.cid];
}
delete eventObject.timeout;
var callback = eventObject.callback;
if (callback) {
delete eventObject.callback;
var error = new TimeoutError("Event response for '" + eventObject.event + "' timed out");
callback.call(eventObject, error, eventObject);
}
};
// 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.emitObject(eventObject, options);
}
return cid;
};
SCTransport.prototype.cancelPendingResponse = function (cid) {
delete this._callbackMap[cid];
};
SCTransport.prototype.decode = function (message) {
return this.codec.decode(message);
};
SCTransport.prototype.encode = function (object) {
return this.codec.encode(object);
};
SCTransport.prototype.send = function (data) {
if (this.socket.readyState !== this.socket.OPEN) {
this._onClose(1005);
} else {
this.socket.send(data);
}
};
SCTransport.prototype.serializeObject = function (object) {
var str, formatError;
try {
str = this.encode(object);
} catch (err) {
formatError = err;
this._onError(form