UNPKG

happn-primus-wrapper

Version:
1,828 lines (1,567 loc) 96 kB
(function(f){var g;if(typeof window!=='undefined'){g=window}else if(typeof self!=='undefined'){g=self}g.Primus=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){ 'use strict'; /** * Create a function that will cleanup the instance. * * @param {Array|String} keys Properties on the instance that needs to be cleared. * @param {Object} options Additional configuration. * @returns {Function} Destroy function * @api public */ module.exports = function demolish(keys, options) { var split = /[, ]+/; options = options || {}; keys = keys || []; if ('string' === typeof keys) keys = keys.split(split); /** * Run addition cleanup hooks. * * @param {String} key Name of the clean up hook to run. * @param {Mixed} selfie Reference to the instance we're cleaning up. * @api private */ function run(key, selfie) { if (!options[key]) return; if ('string' === typeof options[key]) options[key] = options[key].split(split); if ('function' === typeof options[key]) return options[key].call(selfie); for (var i = 0, type, what; i < options[key].length; i++) { what = options[key][i]; type = typeof what; if ('function' === type) { what.call(selfie); } else if ('string' === type && 'function' === typeof selfie[what]) { selfie[what](); } } } /** * Destroy the instance completely and clean up all the existing references. * * @returns {Boolean} * @api public */ return function destroy() { var selfie = this , i = 0 , prop; if (selfie[keys[0]] === null) return false; run('before', selfie); for (; i < keys.length; i++) { prop = keys[i]; if (selfie[prop]) { if ('function' === typeof selfie[prop].destroy) selfie[prop].destroy(); selfie[prop] = null; } } if (selfie.emit) selfie.emit('destroy'); run('after', selfie); return true; }; }; },{}],2:[function(_dereq_,module,exports){ 'use strict'; /** * Returns a function that when invoked executes all the listeners of the * given event with the given arguments. * * @returns {Function} The function that emits all the things. * @api public */ module.exports = function emits() { var self = this , parser; for (var i = 0, l = arguments.length, args = new Array(l); i < l; i++) { args[i] = arguments[i]; } // // If the last argument is a function, assume that it's a parser. // if ('function' !== typeof args[args.length - 1]) return function emitter() { for (var i = 0, l = arguments.length, arg = new Array(l); i < l; i++) { arg[i] = arguments[i]; } return self.emit.apply(self, args.concat(arg)); }; parser = args.pop(); /** * The actual function that emits the given event. It returns a boolean * indicating if the event was emitted. * * @returns {Boolean} * @api public */ return function emitter() { for (var i = 0, l = arguments.length, arg = new Array(l + 1); i < l; i++) { arg[i + 1] = arguments[i]; } /** * Async completion method for the parser. * * @param {Error} err Optional error when parsing failed. * @param {Mixed} returned Emit instructions. * @api private */ arg[0] = function next(err, returned) { if (err) return self.emit('error', err); arg = returned === undefined ? arg.slice(1) : returned === null ? [] : returned; self.emit.apply(self, args.concat(arg)); }; parser.apply(self, arg); return true; }; }; },{}],3:[function(_dereq_,module,exports){ 'use strict'; var has = Object.prototype.hasOwnProperty , prefix = '~'; /** * Constructor to create a storage for our `EE` objects. * An `Events` instance is a plain object whose properties are event names. * * @constructor * @private */ function Events() {} // // We try to not inherit from `Object.prototype`. In some engines creating an // instance in this way is faster than calling `Object.create(null)` directly. // If `Object.create(null)` is not supported we prefix the event names with a // character to make sure that the built-in object properties are not // overridden or used as an attack vector. // if (Object.create) { Events.prototype = Object.create(null); // // This hack is needed because the `__proto__` property is still inherited in // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. // if (!new Events().__proto__) prefix = false; } /** * Representation of a single event listener. * * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} [once=false] Specify if the listener is a one-time listener. * @constructor * @private */ function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } /** * Add a listener for a given event. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} context The context to invoke the listener with. * @param {Boolean} once Specify if the listener is a one-time listener. * @returns {EventEmitter} * @private */ function addListener(emitter, event, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('The listener must be a function'); } var listener = new EE(fn, context || emitter, once) , evt = prefix ? prefix + event : event; if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); else emitter._events[evt] = [emitter._events[evt], listener]; return emitter; } /** * Clear event by name. * * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. * @param {(String|Symbol)} evt The Event name. * @private */ function clearEvent(emitter, evt) { if (--emitter._eventsCount === 0) emitter._events = new Events(); else delete emitter._events[evt]; } /** * Minimal `EventEmitter` interface that is molded against the Node.js * `EventEmitter` interface. * * @constructor * @public */ function EventEmitter() { this._events = new Events(); this._eventsCount = 0; } /** * Return an array listing the events for which the emitter has registered * listeners. * * @returns {Array} * @public */ EventEmitter.prototype.eventNames = function eventNames() { var names = [] , events , name; if (this._eventsCount === 0) return names; for (name in (events = this._events)) { if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); } if (Object.getOwnPropertySymbols) { return names.concat(Object.getOwnPropertySymbols(events)); } return names; }; /** * Return the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Array} The registered listeners. * @public */ EventEmitter.prototype.listeners = function listeners(event) { var evt = prefix ? prefix + event : event , handlers = this._events[evt]; if (!handlers) return []; if (handlers.fn) return [handlers.fn]; for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { ee[i] = handlers[i].fn; } return ee; }; /** * Return the number of listeners listening to a given event. * * @param {(String|Symbol)} event The event name. * @returns {Number} The number of listeners. * @public */ EventEmitter.prototype.listenerCount = function listenerCount(event) { var evt = prefix ? prefix + event : event , listeners = this._events[evt]; if (!listeners) return 0; if (listeners.fn) return 1; return listeners.length; }; /** * Calls each of the listeners registered for a given event. * * @param {(String|Symbol)} event The event name. * @returns {Boolean} `true` if the event had listeners, else `false`. * @public */ EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return false; var listeners = this._events[evt] , len = arguments.length , args , i; if (listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, args); } } } return true; }; /** * Add a listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.on = function on(event, fn, context) { return addListener(this, event, fn, context, false); }; /** * Add a one-time listener for a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn The listener function. * @param {*} [context=this] The context to invoke the listener with. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.once = function once(event, fn, context) { return addListener(this, event, fn, context, true); }; /** * Remove the listeners of a given event. * * @param {(String|Symbol)} event The event name. * @param {Function} fn Only remove the listeners that match this function. * @param {*} context Only remove the listeners that have this context. * @param {Boolean} once Only remove one-time listeners. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { var evt = prefix ? prefix + event : event; if (!this._events[evt]) return this; if (!fn) { clearEvent(this, evt); return this; } var listeners = this._events[evt]; if (listeners.fn) { if ( listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context) ) { clearEvent(this, evt); } } else { for (var i = 0, events = [], length = listeners.length; i < length; i++) { if ( listeners[i].fn !== fn || (once && !listeners[i].once) || (context && listeners[i].context !== context) ) { events.push(listeners[i]); } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; else clearEvent(this, evt); } return this; }; /** * Remove all listeners, or those of the specified event. * * @param {(String|Symbol)} [event] The event name. * @returns {EventEmitter} `this`. * @public */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { var evt; if (event) { evt = prefix ? prefix + event : event; if (this._events[evt]) clearEvent(this, evt); } else { this._events = new Events(); this._eventsCount = 0; } return this; }; // // Alias methods names because people roll like that. // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; // // Expose the prefix. // EventEmitter.prefixed = prefix; // // Allow `EventEmitter` to be imported as module namespace. // EventEmitter.EventEmitter = EventEmitter; // // Expose the module. // if ('undefined' !== typeof module) { module.exports = EventEmitter; } },{}],4:[function(_dereq_,module,exports){ if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module module.exports = function inherits(ctor, superCtor) { if (superCtor) { ctor.super_ = superCtor ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }) } }; } else { // old school shim for old browsers module.exports = function inherits(ctor, superCtor) { if (superCtor) { ctor.super_ = superCtor var TempCtor = function () {} TempCtor.prototype = superCtor.prototype ctor.prototype = new TempCtor() ctor.prototype.constructor = ctor } } } },{}],5:[function(_dereq_,module,exports){ 'use strict'; var regex = new RegExp('^((?:\\d+)?\\.?\\d+) *('+ [ 'milliseconds?', 'msecs?', 'ms', 'seconds?', 'secs?', 's', 'minutes?', 'mins?', 'm', 'hours?', 'hrs?', 'h', 'days?', 'd', 'weeks?', 'wks?', 'w', 'years?', 'yrs?', 'y' ].join('|') +')?$', 'i'); var second = 1000 , minute = second * 60 , hour = minute * 60 , day = hour * 24 , week = day * 7 , year = day * 365; /** * Parse a time string and return the number value of it. * * @param {String} ms Time string. * @returns {Number} * @api private */ module.exports = function millisecond(ms) { var type = typeof ms , amount , match; if ('number' === type) return ms; else if ('string' !== type || '0' === ms || !ms) return 0; else if (+ms) return +ms; // // We are vulnerable to the regular expression denial of service (ReDoS). // In order to mitigate this we don't parse the input string if it is too long. // See https://nodesecurity.io/advisories/46. // if (ms.length > 10000 || !(match = regex.exec(ms))) return 0; amount = parseFloat(match[1]); switch (match[2].toLowerCase()) { case 'years': case 'year': case 'yrs': case 'yr': case 'y': return amount * year; case 'weeks': case 'week': case 'wks': case 'wk': case 'w': return amount * week; case 'days': case 'day': case 'd': return amount * day; case 'hours': case 'hour': case 'hrs': case 'hr': case 'h': return amount * hour; case 'minutes': case 'minute': case 'mins': case 'min': case 'm': return amount * minute; case 'seconds': case 'second': case 'secs': case 'sec': case 's': return amount * second; default: return amount; } }; },{}],6:[function(_dereq_,module,exports){ 'use strict'; /** * Wrap callbacks to prevent double execution. * * @param {Function} fn Function that should only be called once. * @returns {Function} A wrapped callback which prevents execution. * @api public */ module.exports = function one(fn) { var called = 0 , value; /** * The function that prevents double execution. * * @api private */ function onetime() { if (called) return value; called = 1; value = fn.apply(this, arguments); fn = null; return value; } // // To make debugging more easy we want to use the name of the supplied // function. So when you look at the functions that are assigned to event // listeners you don't see a load of `onetime` functions but actually the // names of the functions that this module will call. // onetime.displayName = fn.displayName || fn.name || onetime.displayName || onetime.name; return onetime; }; },{}],7:[function(_dereq_,module,exports){ /*globals require, define */ 'use strict'; var EventEmitter = _dereq_('eventemitter3') , TickTock = _dereq_('tick-tock') , Recovery = _dereq_('recovery') , qs = _dereq_('querystringify') , inherits = _dereq_('inherits') , destroy = _dereq_('demolish') , yeast = _dereq_('yeast') , u2028 = /\u2028/g , u2029 = /\u2029/g; /** * Context assertion, ensure that some of our public Primus methods are called * with the correct context to ensure that * * @param {Primus} self The context of the function. * @param {String} method The method name. * @api private */ function context(self, method) { if (self instanceof Primus) return; var failure = new Error('Primus#'+ method + '\'s context should called with a Primus instance'); if ('function' !== typeof self.listeners || !self.listeners('error').length) { throw failure; } self.emit('error', failure); } // // Sets the default connection URL, it uses the default origin of the browser // when supported but degrades for older browsers. In Node.js, we cannot guess // where the user wants to connect to, so we just default to localhost. // var defaultUrl; try { if (location.origin) { defaultUrl = location.origin; } else { defaultUrl = location.protocol +'//'+ location.host; } } catch (e) { defaultUrl = 'http://127.0.0.1'; } /** * Primus is a real-time library agnostic framework for establishing real-time * connections with servers. * * Options: * - reconnect, configuration for the reconnect process. * - manual, don't automatically call `.open` to start the connection. * - websockets, force the use of WebSockets, even when you should avoid them. * - timeout, connect timeout, server didn't respond in a timely manner. * - pingTimeout, The maximum amount of time to wait for the server to send a ping. * - network, Use network events as leading method for network connection drops. * - strategy, Reconnection strategies. * - transport, Transport options. * - url, uri, The URL to use connect with the server. * * @constructor * @param {String} url The URL of your server. * @param {Object} options The configuration. * @api public */ function Primus(url, options) { if (!(this instanceof Primus)) return new Primus(url, options); Primus.Stream.call(this); if ('function' !== typeof this.client) { return this.critical(new Error( 'The client library has not been compiled correctly, see '+ 'https://github.com/primus/primus#client-library for more details' )); } if ('object' === typeof url) { options = url; url = options.url || options.uri || defaultUrl; } else { options = options || {}; } if ('ping' in options || 'pong' in options) { return this.critical(new Error( 'The `ping` and `pong` options have been removed' )); } var primus = this; // The maximum number of messages that can be placed in queue. options.queueSize = 'queueSize' in options ? options.queueSize : Infinity; // Connection timeout duration. options.timeout = 'timeout' in options ? options.timeout : 10e3; // Stores the back off configuration. options.reconnect = 'reconnect' in options ? options.reconnect : {}; // Heartbeat ping interval. options.pingTimeout = 'pingTimeout' in options ? options.pingTimeout : 45e3; // Reconnect strategies. options.strategy = 'strategy' in options ? options.strategy : []; // Custom transport options. options.transport = 'transport' in options ? options.transport : {}; primus.buffer = []; // Stores premature send data. primus.writable = true; // Silly stream compatibility. primus.readable = true; // Silly stream compatibility. primus.url = primus.parse(url || defaultUrl); // Parse the URL to a readable format. primus.readyState = Primus.CLOSED; // The readyState of the connection. primus.options = options; // Reference to the supplied options. primus.timers = new TickTock(this); // Contains all our timers. primus.socket = null; // Reference to the internal connection. primus.disconnect = false; // Did we receive a disconnect packet? primus.transport = options.transport; // Transport options. primus.transformers = { // Message transformers. outgoing: [], incoming: [] }; // // Create our reconnection instance. // primus.recovery = new Recovery(options.reconnect); // // Parse the reconnection strategy. It can have the following strategies: // // - timeout: Reconnect when we have a network timeout. // - disconnect: Reconnect when we have an unexpected disconnect. // - online: Reconnect when we're back online. // if ('string' === typeof options.strategy) { options.strategy = options.strategy.split(/\s?,\s?/g); } if (false === options.strategy) { // // Strategies are disabled, but we still need an empty array to join it in // to nothing. // options.strategy = []; } else if (!options.strategy.length) { options.strategy.push('disconnect', 'online'); // // Timeout based reconnection should only be enabled conditionally. When // authorization is enabled it could trigger. // if (!this.authorization) options.strategy.push('timeout'); } options.strategy = options.strategy.join(',').toLowerCase(); // // Force the use of WebSockets, even when we've detected some potential // broken WebSocket implementation. // if ('websockets' in options) { primus.AVOID_WEBSOCKETS = !options.websockets; } // // Force or disable the use of NETWORK events as leading client side // disconnection detection. // if ('network' in options) { primus.NETWORK_EVENTS = options.network; } // // Check if the user wants to manually initialise a connection. If they don't, // we want to do it after a really small timeout so we give the users enough // time to listen for `error` events etc. // if (!options.manual) primus.timers.setTimeout('open', function open() { primus.timers.clear('open'); primus.open(); }, 0); primus.initialise(options); } /** * Simple require wrapper to make browserify, node and require.js play nice. * * @param {String} name The module to require. * @returns {Object|Undefined} The module that we required. * @api private */ Primus.requires = Primus.require = function requires(name) { if ('function' !== typeof _dereq_) return undefined; return !('function' === typeof define && define.amd) ? _dereq_(name) : undefined; }; // // It's possible that we're running in Node.js or in a Node.js compatible // environment. In this cases we try to inherit from the Stream base class. // try { Primus.Stream = Primus.requires('stream'); } catch (e) { } if (!Primus.Stream) Primus.Stream = EventEmitter; inherits(Primus, Primus.Stream); /** * Primus readyStates, used internally to set the correct ready state. * * @type {Number} * @private */ Primus.OPENING = 1; // We're opening the connection. Primus.CLOSED = 2; // No active connection. Primus.OPEN = 3; // The connection is open. /** * Are we working with a potentially broken WebSockets implementation? This * boolean can be used by transformers to remove `WebSockets` from their * supported transports. * * @type {Boolean} * @private */ Primus.prototype.AVOID_WEBSOCKETS = false; /** * Some browsers support registering emitting `online` and `offline` events when * the connection has been dropped on the client. We're going to detect it in * a simple `try {} catch (e) {}` statement so we don't have to do complicated * feature detection. * * @type {Boolean} * @private */ Primus.prototype.NETWORK_EVENTS = false; Primus.prototype.online = true; try { if ( Primus.prototype.NETWORK_EVENTS = 'onLine' in navigator && (window.addEventListener || document.body.attachEvent) ) { if (!navigator.onLine) { Primus.prototype.online = false; } } } catch (e) { } /** * The Ark contains all our plugins definitions. It's namespaced by * name => plugin. * * @type {Object} * @private */ Primus.prototype.ark = {}; /** * Simple emit wrapper that returns a function that emits an event once it's * called. This makes it easier for transports to emit specific events. * * @returns {Function} A function that will emit the event when called. * @api public */ Primus.prototype.emits = _dereq_('emits'); /** * Return the given plugin. * * @param {String} name The name of the plugin. * @returns {Object|undefined} The plugin or undefined. * @api public */ Primus.prototype.plugin = function plugin(name) { context(this, 'plugin'); if (name) return this.ark[name]; var plugins = {}; for (name in this.ark) { plugins[name] = this.ark[name]; } return plugins; }; /** * Checks if the given event is an emitted event by Primus. * * @param {String} evt The event name. * @returns {Boolean} Indication of the event is reserved for internal use. * @api public */ Primus.prototype.reserved = function reserved(evt) { return (/^(incoming|outgoing)::/).test(evt) || evt in this.reserved.events; }; /** * The actual events that are used by the client. * * @type {Object} * @public */ Primus.prototype.reserved.events = { 'reconnect scheduled': 1, 'reconnect timeout': 1, 'readyStateChange': 1, 'reconnect failed': 1, 'reconnected': 1, 'reconnect': 1, 'offline': 1, 'timeout': 1, 'destroy': 1, 'online': 1, 'error': 1, 'close': 1, 'open': 1, 'data': 1, 'end': 1 }; /** * Initialise the Primus and setup all parsers and internal listeners. * * @param {Object} options The original options object. * @returns {Primus} * @api private */ Primus.prototype.initialise = function initialise(options) { var primus = this; primus.recovery .on('reconnected', primus.emits('reconnected')) .on('reconnect failed', primus.emits('reconnect failed', function failed(next) { primus.emit('end'); next(); })) .on('reconnect timeout', primus.emits('reconnect timeout')) .on('reconnect scheduled', primus.emits('reconnect scheduled')) .on('reconnect', primus.emits('reconnect', function reconnect(next) { primus.emit('outgoing::reconnect'); next(); })); primus.on('outgoing::open', function opening() { var readyState = primus.readyState; primus.readyState = Primus.OPENING; if (readyState !== primus.readyState) { primus.emit('readyStateChange', 'opening'); } }); primus.on('incoming::open', function opened() { var readyState = primus.readyState; if (primus.recovery.reconnecting()) { primus.recovery.reconnected(); } // // The connection has been opened so we should set our state to // (writ|read)able so our stream compatibility works as intended. // primus.writable = true; primus.readable = true; // // Make sure we are flagged as `online` as we've successfully opened the // connection. // if (!primus.online) { primus.online = true; primus.emit('online'); } primus.readyState = Primus.OPEN; if (readyState !== primus.readyState) { primus.emit('readyStateChange', 'open'); } primus.heartbeat(); if (primus.buffer.length) { var data = primus.buffer.slice() , length = data.length , i = 0; primus.buffer.length = 0; for (; i < length; i++) { primus._write(data[i]); } } primus.emit('open'); }); primus.on('incoming::ping', function ping(time) { primus.online = true; primus.heartbeat(); primus.emit('outgoing::pong', time); primus._write('primus::pong::'+ time); }); primus.on('incoming::error', function error(e) { var connect = primus.timers.active('connect') , err = e; // // When the error is not an Error instance we try to normalize it. // if ('string' === typeof e) { err = new Error(e); } else if (!(e instanceof Error) && 'object' === typeof e) { // // BrowserChannel and SockJS returns an object which contains some // details of the error. In order to have a proper error we "copy" the // details in an Error instance. // err = new Error(e.message || e.reason); for (var key in e) { if (Object.prototype.hasOwnProperty.call(e, key)) err[key] = e[key]; } } // // We're still doing a reconnect attempt, it could be that we failed to // connect because the server was down. Failing connect attempts should // always emit an `error` event instead of a `open` event. // // if (primus.recovery.reconnecting()) return primus.recovery.reconnected(err); if (primus.listeners('error').length) primus.emit('error', err); // // We received an error while connecting, this most likely the result of an // unauthorized access to the server. // if (connect) { if (~primus.options.strategy.indexOf('timeout')) { primus.recovery.reconnect(); } else { primus.end(); } } }); primus.on('incoming::data', function message(raw) { primus.decoder(raw, function decoding(err, data) { // // Do a "safe" emit('error') when we fail to parse a message. We don't // want to throw here as listening to errors should be optional. // if (err) return primus.listeners('error').length && primus.emit('error', err); // // Handle all "primus::" prefixed protocol messages. // if (primus.protocol(data)) return; primus.transforms(primus, primus, 'incoming', data, raw); }); }); primus.on('incoming::end', function end() { var readyState = primus.readyState; // // This `end` started with the receiving of a primus::server::close packet // which indicated that the user/developer on the server closed the // connection and it was not a result of a network disruption. So we should // kill the connection without doing a reconnect. // if (primus.disconnect) { primus.disconnect = false; return primus.end(); } // // Always set the readyState to closed, and if we're still connecting, close // the connection so we're sure that everything after this if statement block // is only executed because our readyState is set to `open`. // primus.readyState = Primus.CLOSED; if (readyState !== primus.readyState) { primus.emit('readyStateChange', 'end'); } if (primus.timers.active('connect')) primus.end(); if (readyState !== Primus.OPEN) { return primus.recovery.reconnecting() ? primus.recovery.reconnect() : false; } this.writable = false; this.readable = false; // // Clear all timers in case we're not going to reconnect. // this.timers.clear(); // // Fire the `close` event as an indication of connection disruption. // This is also fired by `primus#end` so it is emitted in all cases. // primus.emit('close'); // // The disconnect was unintentional, probably because the server has // shutdown, so if the reconnection is enabled start a reconnect procedure. // if (~primus.options.strategy.indexOf('disconnect')) { return primus.recovery.reconnect(); } primus.emit('outgoing::end'); primus.emit('end'); }); // // Setup the real-time client. // primus.client(); // // Process the potential plugins. // for (var plugin in primus.ark) { primus.ark[plugin].call(primus, primus, options); } // // NOTE: The following code is only required if we're supporting network // events as it requires access to browser globals. // if (!primus.NETWORK_EVENTS) return primus; /** * Handler for offline notifications. * * @api private */ primus.offlineHandler = function offline() { if (!primus.online) return; // Already or still offline, bailout. primus.online = false; primus.emit('offline'); primus.end(); // // It is certainly possible that we're in a reconnection loop and that the // user goes offline. In this case we want to kill the existing attempt so // when the user goes online, it will attempt to reconnect freshly again. // primus.recovery.reset(); }; /** * Handler for online notifications. * * @api private */ primus.onlineHandler = function online() { if (primus.online) return; // Already or still online, bailout. primus.online = true; primus.emit('online'); if (~primus.options.strategy.indexOf('online')) { primus.recovery.reconnect(); } }; if (window.addEventListener) { window.addEventListener('offline', primus.offlineHandler, false); window.addEventListener('online', primus.onlineHandler, false); } else if (document.body.attachEvent){ document.body.attachEvent('onoffline', primus.offlineHandler); document.body.attachEvent('ononline', primus.onlineHandler); } return primus; }; /** * Really dead simple protocol parser. We simply assume that every message that * is prefixed with `primus::` could be used as some sort of protocol definition * for Primus. * * @param {String} msg The data. * @returns {Boolean} Is a protocol message. * @api private */ Primus.prototype.protocol = function protocol(msg) { if ( 'string' !== typeof msg || msg.indexOf('primus::') !== 0 ) return false; var last = msg.indexOf(':', 8) , value = msg.slice(last + 2); switch (msg.slice(8, last)) { case 'ping': this.emit('incoming::ping', +value); break; case 'server': // // The server is closing the connection, forcefully disconnect so we don't // reconnect again. // if ('close' === value) { this.disconnect = true; } break; case 'id': this.emit('incoming::id', value); break; // // Unknown protocol, somebody is probably sending `primus::` prefixed // messages. // default: return false; } return true; }; /** * Execute the set of message transformers from Primus on the incoming or * outgoing message. * This function and it's content should be in sync with Spark#transforms in * spark.js. * * @param {Primus} primus Reference to the Primus instance with message transformers. * @param {Spark|Primus} connection Connection that receives or sends data. * @param {String} type The type of message, 'incoming' or 'outgoing'. * @param {Mixed} data The data to send or that has been received. * @param {String} raw The raw encoded data. * @returns {Primus} * @api public */ Primus.prototype.transforms = function transforms(primus, connection, type, data, raw) { var packet = { data: data } , fns = primus.transformers[type]; // // Iterate in series over the message transformers so we can allow optional // asynchronous execution of message transformers which could for example // retrieve additional data from the server, do extra decoding or even // message validation. // (function transform(index, done) { var transformer = fns[index++]; if (!transformer) return done(); if (1 === transformer.length) { if (false === transformer.call(connection, packet)) { // // When false is returned by an incoming transformer it means that's // being handled by the transformer and we should not emit the `data` // event. // return; } return transform(index, done); } transformer.call(connection, packet, function finished(err, arg) { if (err) return connection.emit('error', err); if (false === arg) return; transform(index, done); }); }(0, function done() { // // We always emit 2 arguments for the data event, the first argument is the // parsed data and the second argument is the raw string that we received. // This allows you, for example, to do some validation on the parsed data // and then save the raw string in your database without the stringify // overhead. // if ('incoming' === type) return connection.emit('data', packet.data, raw); connection._write(packet.data); })); return this; }; /** * Retrieve the current id from the server. * * @param {Function} fn Callback function. * @returns {Primus} * @api public */ Primus.prototype.id = function id(fn) { if (this.socket && this.socket.id) return fn(this.socket.id); this._write('primus::id::'); return this.once('incoming::id', fn); }; /** * Establish a connection with the server. When this function is called we * assume that we don't have any open connections. If you do call it when you * have a connection open, it could cause duplicate connections. * * @returns {Primus} * @api public */ Primus.prototype.open = function open() { context(this, 'open'); // // Only start a `connection timeout` procedure if we're not reconnecting as // that shouldn't count as an initial connection. This should be started // before the connection is opened to capture failing connections and kill the // timeout. // if (!this.recovery.reconnecting() && this.options.timeout) this.timeout(); this.emit('outgoing::open'); return this; }; /** * Send a new message. * * @param {Mixed} data The data that needs to be written. * @returns {Boolean} Always returns true as we don't support back pressure. * @api public */ Primus.prototype.write = function write(data) { context(this, 'write'); this.transforms(this, this, 'outgoing', data); return true; }; /** * The actual message writer. * * @param {Mixed} data The message that needs to be written. * @returns {Boolean} Successful write to the underlaying transport. * @api private */ Primus.prototype._write = function write(data) { var primus = this; // // The connection is closed, normally this would already be done in the // `spark.write` method, but as `_write` is used internally, we should also // add the same check here to prevent potential crashes by writing to a dead // socket. // if (Primus.OPEN !== primus.readyState) { // // If the buffer is at capacity, remove the first item. // if (this.buffer.length === this.options.queueSize) { this.buffer.splice(0, 1); } this.buffer.push(data); return false; } primus.encoder(data, function encoded(err, packet) { // // Do a "safe" emit('error') when we fail to parse a message. We don't // want to throw here as listening to errors should be optional. // if (err) return primus.listeners('error').length && primus.emit('error', err); // // Hack 1: \u2028 and \u2029 are allowed inside a JSON string, but JavaScript // defines them as newline separators. Unescaped control characters are not // allowed inside JSON strings, so this causes an error at parse time. We // work around this issue by escaping these characters. This can cause // errors with JSONP requests or if the string is just evaluated. // if ('string' === typeof packet) { if (~packet.indexOf('\u2028')) packet = packet.replace(u2028, '\\u2028'); if (~packet.indexOf('\u2029')) packet = packet.replace(u2029, '\\u2029'); } primus.emit('outgoing::data', packet); }); return true; }; /** * Set a timer that, upon expiration, closes the client. * * @returns {Primus} * @api private */ Primus.prototype.heartbeat = function heartbeat() { if (!this.options.pingTimeout) return this; this.timers.clear('heartbeat'); this.timers.setTimeout('heartbeat', function expired() { // // The network events already captured the offline event. // if (!this.online) return; this.online = false; this.emit('offline'); this.emit('incoming::end'); }, this.options.pingTimeout); return this; }; /** * Start a connection timeout. * * @returns {Primus} * @api private */ Primus.prototype.timeout = function timeout() { var primus = this; /** * Remove all references to the timeout listener as we've received an event * that can be used to determine state. * * @api private */ function remove() { primus.removeListener('error', remove) .removeListener('open', remove) .removeListener('end', remove) .timers.clear('connect'); } primus.timers.setTimeout('connect', function expired() { remove(); // Clean up old references. if (primus.readyState === Primus.OPEN || primus.recovery.reconnecting()) { return; } primus.emit('timeout'); // // We failed to connect to the server. // if (~primus.options.strategy.indexOf('timeout')) { primus.recovery.reconnect(); } else { primus.end(); } }, primus.options.timeout); return primus.on('error', remove) .on('open', remove) .on('end', remove); }; /** * Close the connection completely. * * @param {Mixed} data last packet of data. * @returns {Primus} * @api public */ Primus.prototype.end = function end(data) { context(this, 'end'); if ( this.readyState === Primus.CLOSED && !this.timers.active('connect') && !this.timers.active('open') ) { // // If we are reconnecting stop the reconnection procedure. // if (this.recovery.reconnecting()) { this.recovery.reset(); this.emit('end'); } return this; } if (data !== undefined) this.write(data); this.writable = false; this.readable = false; var readyState = this.readyState; this.readyState = Primus.CLOSED; if (readyState !== this.readyState) { this.emit('readyStateChange', 'end'); } this.timers.clear(); this.emit('outgoing::end'); this.emit('close'); this.emit('end'); return this; }; /** * Completely demolish the Primus instance and forcefully nuke all references. * * @returns {Boolean} * @api public */ Primus.prototype.destroy = destroy('url timers options recovery socket transport transformers', { before: 'end', after: ['removeAllListeners', function detach() { if (!this.NETWORK_EVENTS) return; if (window.addEventListener) { window.removeEventListener('offline', this.offlineHandler); window.removeEventListener('online', this.onlineHandler); } else if (document.body.attachEvent){ document.body.detachEvent('onoffline', this.offlineHandler); document.body.detachEvent('ononline', this.onlineHandler); } }] }); /** * Create a shallow clone of a given object. * * @param {Object} obj The object that needs to be cloned. * @returns {Object} Copy. * @api private */ Primus.prototype.clone = function clone(obj) { return this.merge({}, obj); }; /** * Merge different objects in to one target object. * * @param {Object} target The object where everything should be merged in. * @returns {Object} Original target with all merged objects. * @api private */ Primus.prototype.merge = function merge(target) { for (var i = 1, key, obj; i < arguments.length; i++) { obj = arguments[i]; for (key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) target[key] = obj[key]; } } return target; }; /** * Parse the connection string. * * @type {Function} * @param {String} url Connection URL. * @returns {Object} Parsed connection. * @api private */ Primus.prototype.parse = _dereq_('url-parse'); /** * Parse a query string. * * @param {String} query The query string that needs to be parsed. * @returns {Object} Parsed query string. * @api private */ Primus.prototype.querystring = qs.parse; /** * Transform a query string object back into string equiv. * * @param {Object} obj The query string object. * @returns {String} * @api private */ Primus.prototype.querystringify = qs.stringify; /** * Generates a connection URI. * * @param {String} protocol The protocol that should used to crate the URI. * @returns {String|options} The URL. * @api private */ Primus.prototype.uri = function uri(options) { var url = this.url , server = [] , qsa = false; // // Query strings are only allowed when we've received clearance for it. // if (options.query) qsa = true; options = options || {}; options.protocol = 'protocol' in options ? options.protocol : 'http:'; options.query = url.query && qsa ? url.query.slice(1) : false; options.secure = 'secure' in options ? options.secure : url.protocol === 'https:' || url.protocol === 'wss:'; options.auth = 'auth' in options ? options.auth : url.auth; options.pathname = 'pathname' in options ? options.pathname : this.pathname; options.port = 'port' in options ? +options.port : +url.port || (options.secure ? 443 : 80); // // We need to make sure that we create a unique connection URL every time to // prevent back forward cache from becoming an issue. We're doing this by // forcing an cache busting query string in to the URL. // var querystring = this.querystring(options.query || ''); querystring._primuscb = yeast(); options.query = this.querystringify(querystring); // // Allow transformation of the options before we construct a full URL from it. // this.emit('outgoing::url', options); // // Automatically suffix the protocol so we can supply `ws:` and `http:` and // it gets transformed correctly. // server.push(options.secure ? options.protocol.replace(':', 's:') : options.protocol, ''); server.push(options.auth ? options.auth +'@'+ url.host : url.host); // // Pathnames are optional as some Transformers would just use the pathname // directly. // if (options.pathname) server.push(options.pathname.slice(1)); // // Optionally add a search query. // if (qsa) server[server.length - 1] += '?'+ options.query; else delete options.query; if (options.object) return options; return server.join('/'); }; /** * Register a new message transformer. This allows you to easily manipulate incoming * and outgoing data which is particularity handy for plugins that want to send * meta data together with the messages. * * @param {String} type Incoming or outgoing * @param {Function} fn A new message transformer. * @returns {Primus} * @api public */ Primus.prototype.transform = function transform(type, fn) { context(this, 'transform'); if (!(type in this.transformers)) { return this.critical(new Error('Invalid transformer type')); } this.transformers[type].push(fn); return this; }; /** * A critical error has occurred, if we have an `error` listener, emit it there. * If not, throw it, so we get a stack trace + proper error message. * * @param {Error} err The critical error. * @returns {Primus} * @api private */ Primus.prototype.critical = function critical(err) { if (this.emit('error', err)) return this; throw err; }; /** * Syntax sugar, adopt a Socket.IO like API. * * @param {String} url The URL we want to connect to. * @param {Object} options Connection options. * @returns {Primus} * @api public */ Primus.connect = function connect(url, options) { return new Primus(url, options); }; // // Expose the EventEmitter so it can be re-used by wrapping libraries we're also // exposing the Stream interface. // Primus.EventEmitter = EventEmitter; // // These libraries are automatically inserted at the server-side using the // Primus#library method. // Primus.prototype.client = null; // @import {primus::client}; Primus.prototype.authorization = null; // @import {primus::auth}; Primus.prototype.pathname = null; // @import {primus::pathname}; Primus.prototype.encoder = null; // @import {primus::encoder}; Primus.prototype.decoder = null; // @import {primus::decoder}; Primus.prototype.version = null; // @import {primus::version}; if ( 'undefined' !== typeof document && 'undefined' !== typeof navigator ) { // // Hack 2: If you press ESC in FireFox it will close all active connections. // Normally this makes sense, when your page is still loading. But versions // before FireFox 22 will close all connections including WebSocket connections // after page load. One way to prevent this is to do a `preventDefault()` and // cancel the operation before it bubbles up to the browsers default handler. // It needs to be added as `keydown` event, if it's added keyup it will not be // able to prevent the connection from being closed. // if (document.addEventListener) { document.addEventListener('keydown', function keydown(e) { if (e.keyCode !== 27 || !e.preventDefault) return; e.preventDefault(); }, false); } // // Hack 3: This is a Mac/Apple bug only, when you're behind a reverse proxy or // have you network settings set to `automatic proxy discovery` the safari // browser will crash when the WebSocke