UNPKG

@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
/** * 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