/*! * Pusher JavaScript Library v2.2.0 * http://pusherapp.com/ * * Copyright 2013, Pusher * Released under the MIT licence. */ ;(function() { function Pusher(app_key, options) { checkAppKey(app_key); options = options || {}; var self = this; this.key = app_key; this.config = Pusher.Util.extend( Pusher.getGlobalConfig(), options.cluster ? Pusher.getClusterConfig(options.cluster) : {}, options ); this.channels = new Pusher.Channels(); this.global_emitter = new Pusher.EventsDispatcher(); this.sessionID = Math.floor(Math.random() * 1000000000); this.timeline = new Pusher.Timeline(this.key, this.sessionID, { cluster: this.config.cluster, features: Pusher.Util.getClientFeatures(), params: this.config.timelineParams || {}, limit: 50, level: Pusher.Timeline.INFO, version: Pusher.VERSION }); if (!this.config.disableStats) { this.timelineSender = new Pusher.TimelineSender(this.timeline, { host: this.config.statsHost, path: "/timeline/v2/jsonp" }); } var getStrategy = function(options) { var config = Pusher.Util.extend({}, self.config, options); return Pusher.StrategyBuilder.build( Pusher.getDefaultStrategy(config), config ); }; this.connection = new Pusher.ConnectionManager( this.key, Pusher.Util.extend( { getStrategy: getStrategy, timeline: this.timeline, activityTimeout: this.config.activity_timeout, pongTimeout: this.config.pong_timeout, unavailableTimeout: this.config.unavailable_timeout }, this.config, { encrypted: this.isEncrypted() } ) ); this.connection.bind('connected', function() { self.subscribeAll(); if (self.timelineSender) { self.timelineSender.send(self.connection.isEncrypted()); } }); this.connection.bind('message', function(params) { var internal = (params.event.indexOf('pusher_internal:') === 0); if (params.channel) { var channel = self.channel(params.channel); if (channel) { channel.handleEvent(params.event, params.data); } } // Emit globaly [deprecated] if (!internal) { self.global_emitter.emit(params.event, params.data); } }); this.connection.bind('disconnected', function() { self.channels.disconnect(); }); this.connection.bind('error', function(err) { Pusher.warn('Error', err); }); Pusher.instances.push(this); this.timeline.info({ instances: Pusher.instances.length }); if (Pusher.isReady) { self.connect(); } } var prototype = Pusher.prototype; Pusher.instances = []; Pusher.isReady = false; // To receive log output provide a Pusher.log function, for example // Pusher.log = function(m){console.log(m)} Pusher.debug = function() { if (!Pusher.log) { return; } Pusher.log(Pusher.Util.stringify.apply(this, arguments)); }; Pusher.warn = function() { var message = Pusher.Util.stringify.apply(this, arguments); if (window.console) { if (window.console.warn) { window.console.warn(message); } else if (window.console.log) { window.console.log(message); } } if (Pusher.log) { Pusher.log(message); } }; Pusher.ready = function() { Pusher.isReady = true; for (var i = 0, l = Pusher.instances.length; i < l; i++) { Pusher.instances[i].connect(); } }; prototype.channel = function(name) { return this.channels.find(name); }; prototype.allChannels = function() { return this.channels.all(); }; prototype.connect = function() { this.connection.connect(); if (this.timelineSender) { if (!this.timelineSenderTimer) { var encrypted = this.connection.isEncrypted(); var timelineSender = this.timelineSender; this.timelineSenderTimer = new Pusher.PeriodicTimer(60000, function() { timelineSender.send(encrypted); }); } } }; prototype.disconnect = function() { this.connection.disconnect(); if (this.timelineSenderTimer) { this.timelineSenderTimer.ensureAborted(); this.timelineSenderTimer = null; } }; prototype.bind = function(event_name, callback) { this.global_emitter.bind(event_name, callback); return this; }; prototype.bind_all = function(callback) { this.global_emitter.bind_all(callback); return this; }; prototype.subscribeAll = function() { var channelName; for (channelName in this.channels.channels) { if (this.channels.channels.hasOwnProperty(channelName)) { this.subscribe(channelName); } } }; prototype.subscribe = function(channel_name) { var channel = this.channels.add(channel_name, this); if (this.connection.state === 'connected') { channel.subscribe(); } return channel; }; prototype.unsubscribe = function(channel_name) { var channel = this.channels.remove(channel_name); if (this.connection.state === 'connected') { channel.unsubscribe(); } }; prototype.send_event = function(event_name, data, channel) { return this.connection.send_event(event_name, data, channel); }; prototype.isEncrypted = function() { if (Pusher.Util.getDocument().location.protocol === "https:") { return true; } else { return Boolean(this.config.encrypted); } }; function checkAppKey(key) { if (key === null || key === undefined) { Pusher.warn( 'Warning', 'You must pass your app key when you instantiate Pusher.' ); } } Pusher.HTTP = {}; this.Pusher = Pusher; }).call(this); ;(function() { // We need to bind clear functions this way to avoid exceptions on IE8 function clearTimeout(timer) { window.clearTimeout(timer); } function clearInterval(timer) { window.clearInterval(timer); } function GenericTimer(set, clear, delay, callback) { var self = this; this.clear = clear; this.timer = set(function() { if (self.timer !== null) { self.timer = callback(self.timer); } }, delay); } var prototype = GenericTimer.prototype; /** Returns whether the timer is still running. * * @return {Boolean} */ prototype.isRunning = function() { return this.timer !== null; }; /** Aborts a timer when it's running. */ prototype.ensureAborted = function() { if (this.timer) { // Clear function is already bound this.clear(this.timer); this.timer = null; } }; /** Cross-browser compatible one-off timer abstraction. * * @param {Number} delay * @param {Function} callback */ Pusher.Timer = function(delay, callback) { return new GenericTimer(setTimeout, clearTimeout, delay, function(timer) { callback(); return null; }); }; /** Cross-browser compatible periodic timer abstraction. * * @param {Number} delay * @param {Function} callback */ Pusher.PeriodicTimer = function(delay, callback) { return new GenericTimer(setInterval, clearInterval, delay, function(timer) { callback(); return timer; }); }; }).call(this); ;(function() { Pusher.Util = { now: function() { if (Date.now) { return Date.now(); } else { return new Date().valueOf(); } }, defer: function(callback) { return new Pusher.Timer(0, callback); }, /** Merges multiple objects into the target argument. * * For properties that are plain Objects, performs a deep-merge. For the * rest it just copies the value of the property. * * To extend prototypes use it as following: * Pusher.Util.extend(Target.prototype, Base.prototype) * * You can also use it to merge objects without altering them: * Pusher.Util.extend({}, object1, object2) * * @param {Object} target * @return {Object} the target argument */ extend: function(target) { for (var i = 1; i < arguments.length; i++) { var extensions = arguments[i]; for (var property in extensions) { if (extensions[property] && extensions[property].constructor && extensions[property].constructor === Object) { target[property] = Pusher.Util.extend( target[property] || {}, extensions[property] ); } else { target[property] = extensions[property]; } } } return target; }, stringify: function() { var m = ["Pusher"]; for (var i = 0; i < arguments.length; i++) { if (typeof arguments[i] === "string") { m.push(arguments[i]); } else { if (window.JSON === undefined) { m.push(arguments[i].toString()); } else { m.push(JSON.stringify(arguments[i])); } } } return m.join(" : "); }, arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf var nativeIndexOf = Array.prototype.indexOf; if (array === null) { return -1; } if (nativeIndexOf && array.indexOf === nativeIndexOf) { return array.indexOf(item); } for (var i = 0, l = array.length; i < l; i++) { if (array[i] === item) { return i; } } return -1; }, /** Applies a function f to all properties of an object. * * Function f gets 3 arguments passed: * - element from the object * - key of the element * - reference to the object * * @param {Object} object * @param {Function} f */ objectApply: function(object, f) { for (var key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { f(object[key], key, object); } } }, /** Return a list of object's own property keys * * @param {Object} object * @returns {Array} */ keys: function(object) { var keys = []; Pusher.Util.objectApply(object, function(_, key) { keys.push(key); }); return keys; }, /** Return a list of object's own property values * * @param {Object} object * @returns {Array} */ values: function(object) { var values = []; Pusher.Util.objectApply(object, function(value) { values.push(value); }); return values; }, /** Applies a function f to all elements of an array. * * Function f gets 3 arguments passed: * - element from the array * - index of the element * - reference to the array * * @param {Array} array * @param {Function} f */ apply: function(array, f, context) { for (var i = 0; i < array.length; i++) { f.call(context || window, array[i], i, array); } }, /** Maps all elements of the array and returns the result. * * Function f gets 4 arguments passed: * - element from the array * - index of the element * - reference to the source array * - reference to the destination array * * @param {Array} array * @param {Function} f */ map: function(array, f) { var result = []; for (var i = 0; i < array.length; i++) { result.push(f(array[i], i, array, result)); } return result; }, /** Maps all elements of the object and returns the result. * * Function f gets 4 arguments passed: * - element from the object * - key of the element * - reference to the source object * - reference to the destination object * * @param {Object} object * @param {Function} f */ mapObject: function(object, f) { var result = {}; Pusher.Util.objectApply(object, function(value, key) { result[key] = f(value); }); return result; }, /** Filters elements of the array using a test function. * * Function test gets 4 arguments passed: * - element from the array * - index of the element * - reference to the source array * - reference to the destination array * * @param {Array} array * @param {Function} f */ filter: function(array, test) { test = test || function(value) { return !!value; }; var result = []; for (var i = 0; i < array.length; i++) { if (test(array[i], i, array, result)) { result.push(array[i]); } } return result; }, /** Filters properties of the object using a test function. * * Function test gets 4 arguments passed: * - element from the object * - key of the element * - reference to the source object * - reference to the destination object * * @param {Object} object * @param {Function} f */ filterObject: function(object, test) { var result = {}; Pusher.Util.objectApply(object, function(value, key) { if ((test && test(value, key, object, result)) || Boolean(value)) { result[key] = value; } }); return result; }, /** Flattens an object into a two-dimensional array. * * @param {Object} object * @return {Array} resulting array of [key, value] pairs */ flatten: function(object) { var result = []; Pusher.Util.objectApply(object, function(value, key) { result.push([key, value]); }); return result; }, /** Checks whether any element of the array passes the test. * * Function test gets 3 arguments passed: * - element from the array * - index of the element * - reference to the source array * * @param {Array} array * @param {Function} f */ any: function(array, test) { for (var i = 0; i < array.length; i++) { if (test(array[i], i, array)) { return true; } } return false; }, /** Checks whether all elements of the array pass the test. * * Function test gets 3 arguments passed: * - element from the array * - index of the element * - reference to the source array * * @param {Array} array * @param {Function} f */ all: function(array, test) { for (var i = 0; i < array.length; i++) { if (!test(array[i], i, array)) { return false; } } return true; }, /** Builds a function that will proxy a method call to its first argument. * * Allows partial application of arguments, so additional arguments are * prepended to the argument list. * * @param {String} name method name * @return {Function} proxy function */ method: function(name) { var boundArguments = Array.prototype.slice.call(arguments, 1); return function(object) { return object[name].apply(object, boundArguments.concat(arguments)); }; }, getWindow: function() { return window; }, getDocument: function() { return document; }, getNavigator: function() { return navigator; }, getLocalStorage: function() { try { return window.localStorage; } catch (e) { return undefined; } }, getClientFeatures: function() { return Pusher.Util.keys( Pusher.Util.filterObject( { "ws": Pusher.WSTransport, "flash": Pusher.FlashTransport }, function (t) { return t.isSupported({}); } ) ); }, addWindowListener: function(event, listener) { var _window = Pusher.Util.getWindow(); if (_window.addEventListener !== undefined) { _window.addEventListener(event, listener, false); } else { _window.attachEvent("on" + event, listener); } }, removeWindowListener: function(event, listener) { var _window = Pusher.Util.getWindow(); if (_window.addEventListener !== undefined) { _window.removeEventListener(event, listener, false); } else { _window.detachEvent("on" + event, listener); } }, isXHRSupported: function() { var XHR = window.XMLHttpRequest; return Boolean(XHR) && (new XHR()).withCredentials !== undefined; }, isXDRSupported: function(encrypted) { var protocol = encrypted ? "https:" : "http:"; var documentProtocol = Pusher.Util.getDocument().location.protocol; return Boolean(window.XDomainRequest) && documentProtocol === protocol; } }; }).call(this); ;(function() { Pusher.VERSION = '2.2.0'; Pusher.PROTOCOL = 7; // DEPRECATED: WS connection parameters Pusher.host = 'ws.pusherapp.com'; Pusher.ws_port = 80; Pusher.wss_port = 443; // DEPRECATED: SockJS fallback parameters Pusher.sockjs_host = 'sockjs.pusher.com'; Pusher.sockjs_http_port = 80; Pusher.sockjs_https_port = 443; Pusher.sockjs_path = "/pusher"; // DEPRECATED: Stats Pusher.stats_host = 'stats.pusher.com'; // DEPRECATED: Other settings Pusher.channel_auth_endpoint = '/pusher/auth'; Pusher.channel_auth_transport = 'ajax'; Pusher.activity_timeout = 120000; Pusher.pong_timeout = 30000; Pusher.unavailable_timeout = 10000; // CDN configuration Pusher.cdn_http = 'http://js.pusher.com/'; Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'; Pusher.dependency_suffix = ''; Pusher.getDefaultStrategy = function(config) { var wsStrategy; if (config.encrypted) { wsStrategy = [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":http_fallback_loop"]] ]; } else { wsStrategy = [ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":wss_loop"]], [":delayed", 5000, [":http_fallback_loop"]] ]; } return [ [":def", "ws_options", { hostUnencrypted: config.wsHost + ":" + config.wsPort, hostEncrypted: config.wsHost + ":" + config.wssPort }], [":def", "wss_options", [":extend", ":ws_options", { encrypted: true }]], [":def", "sockjs_options", { hostUnencrypted: config.httpHost + ":" + config.httpPort, hostEncrypted: config.httpHost + ":" + config.httpsPort }], [":def", "timeouts", { loop: true, timeout: 15000, timeoutLimit: 60000 }], [":def", "ws_manager", [":transport_manager", { lives: 2, minPingDelay: 10000, maxPingDelay: config.activity_timeout }]], [":def", "streaming_manager", [":transport_manager", { lives: 2, minPingDelay: 10000, maxPingDelay: config.activity_timeout }]], [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"], [":def_transport", "wss", "ws", 3, ":wss_options", ":ws_manager"], [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"], [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"], [":def_transport", "xhr_streaming", "xhr_streaming", 1, ":sockjs_options", ":streaming_manager"], [":def_transport", "xdr_streaming", "xdr_streaming", 1, ":sockjs_options", ":streaming_manager"], [":def_transport", "xhr_polling", "xhr_polling", 1, ":sockjs_options"], [":def_transport", "xdr_polling", "xdr_polling", 1, ":sockjs_options"], [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]], [":def", "wss_loop", [":sequential", ":timeouts", ":wss"]], [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]], [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]], [":def", "streaming_loop", [":sequential", ":timeouts", [":if", [":is_supported", ":xhr_streaming"], ":xhr_streaming", ":xdr_streaming" ] ]], [":def", "polling_loop", [":sequential", ":timeouts", [":if", [":is_supported", ":xhr_polling"], ":xhr_polling", ":xdr_polling" ] ]], [":def", "http_loop", [":if", [":is_supported", ":streaming_loop"], [ ":best_connected_ever", ":streaming_loop", [":delayed", 4000, [":polling_loop"]] ], [ ":polling_loop" ]]], [":def", "http_fallback_loop", [":if", [":is_supported", ":http_loop"], [ ":http_loop" ], [ ":sockjs_loop" ]] ], [":def", "strategy", [":cached", 1800000, [":first_connected", [":if", [":is_supported", ":ws"], wsStrategy, [":if", [":is_supported", ":flash"], [ ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":http_fallback_loop"]] ], [ ":http_fallback_loop" ]]] ] ] ] ]; }; }).call(this); ;(function() { Pusher.getGlobalConfig = function() { return { wsHost: Pusher.host, wsPort: Pusher.ws_port, wssPort: Pusher.wss_port, httpHost: Pusher.sockjs_host, httpPort: Pusher.sockjs_http_port, httpsPort: Pusher.sockjs_https_port, httpPath: Pusher.sockjs_path, statsHost: Pusher.stats_host, authEndpoint: Pusher.channel_auth_endpoint, authTransport: Pusher.channel_auth_transport, // TODO make this consistent with other options in next major version activity_timeout: Pusher.activity_timeout, pong_timeout: Pusher.pong_timeout, unavailable_timeout: Pusher.unavailable_timeout }; }; Pusher.getClusterConfig = function(clusterName) { return { wsHost: "ws-" + clusterName + ".pusher.com", httpHost: "sockjs-" + clusterName + ".pusher.com" }; }; }).call(this); ;(function() { function buildExceptionClass(name) { var constructor = function(message) { Error.call(this, message); this.name = name; }; Pusher.Util.extend(constructor.prototype, Error.prototype); return constructor; } /** Error classes used throughout pusher-js library. */ Pusher.Errors = { BadEventName: buildExceptionClass("BadEventName"), RequestTimedOut: buildExceptionClass("RequestTimedOut"), TransportPriorityTooLow: buildExceptionClass("TransportPriorityTooLow"), TransportClosed: buildExceptionClass("TransportClosed"), UnsupportedTransport: buildExceptionClass("UnsupportedTransport"), UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy") }; }).call(this); ;(function() { /** Manages callback bindings and event emitting. * * @param Function failThrough called when no listeners are bound to an event */ function EventsDispatcher(failThrough) { this.callbacks = new CallbackRegistry(); this.global_callbacks = []; this.failThrough = failThrough; } var prototype = EventsDispatcher.prototype; prototype.bind = function(eventName, callback, context) { this.callbacks.add(eventName, callback, context); return this; }; prototype.bind_all = function(callback) { this.global_callbacks.push(callback); return this; }; prototype.unbind = function(eventName, callback, context) { this.callbacks.remove(eventName, callback, context); return this; }; prototype.unbind_all = function(eventName, callback) { this.callbacks.remove(eventName, callback); return this; }; prototype.emit = function(eventName, data) { var i; for (i = 0; i < this.global_callbacks.length; i++) { this.global_callbacks[i](eventName, data); } var callbacks = this.callbacks.get(eventName); if (callbacks && callbacks.length > 0) { for (i = 0; i < callbacks.length; i++) { callbacks[i].fn.call(callbacks[i].context || window, data); } } else if (this.failThrough) { this.failThrough(eventName, data); } return this; }; /** Callback registry helper. */ function CallbackRegistry() { this._callbacks = {}; } CallbackRegistry.prototype.get = function(name) { return this._callbacks[prefix(name)]; }; CallbackRegistry.prototype.add = function(name, callback, context) { var prefixedEventName = prefix(name); this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || []; this._callbacks[prefixedEventName].push({ fn: callback, context: context }); }; CallbackRegistry.prototype.remove = function(name, callback, context) { if (!name && !callback && !context) { this._callbacks = {}; return; } var names = name ? [prefix(name)] : Pusher.Util.keys(this._callbacks); if (callback || context) { Pusher.Util.apply(names, function(name) { this._callbacks[name] = Pusher.Util.filter( this._callbacks[name] || [], function(binding) { return (callback && callback !== binding.fn) || (context && context !== binding.context); } ); if (this._callbacks[name].length === 0) { delete this._callbacks[name]; } }, this); } else { Pusher.Util.apply(names, function(name) { delete this._callbacks[name]; }, this); } }; function prefix(name) { return "_" + name; } Pusher.EventsDispatcher = EventsDispatcher; }).call(this); (function() { /** Builds receivers for JSONP and Script requests. * * Each receiver is an object with following fields: * - number - unique (for the factory instance), numerical id of the receiver * - id - a string ID that can be used in DOM attributes * - name - name of the function triggering the receiver * - callback - callback function * * Receivers are triggered only once, on the first callback call. * * Receivers can be called by their name or by accessing factory object * by the number key. * * @param {String} prefix the prefix used in ids * @param {String} name the name of the object */ function ScriptReceiverFactory(prefix, name) { this.lastId = 0; this.prefix = prefix; this.name = name; } var prototype = ScriptReceiverFactory.prototype; /** Creates a script receiver. * * @param {Function} callback * @return {ScriptReceiver} */ prototype.create = function(callback) { this.lastId++; var number = this.lastId; var id = this.prefix + number; var name = this.name + "[" + number + "]"; var called = false; var callbackWrapper = function() { if (!called) { callback.apply(null, arguments); called = true; } }; this[number] = callbackWrapper; return { number: number, id: id, name: name, callback: callbackWrapper }; }; /** Removes the script receiver from the list. * * @param {ScriptReceiver} receiver */ prototype.remove = function(receiver) { delete this[receiver.number]; }; Pusher.ScriptReceiverFactory = ScriptReceiverFactory; Pusher.ScriptReceivers = new ScriptReceiverFactory( "_pusher_script_", "Pusher.ScriptReceivers" ); }).call(this); (function() { /** Sends a generic HTTP GET request using a script tag. * * By constructing URL in a specific way, it can be used for loading * JavaScript resources or JSONP requests. It can notify about errors, but * only in certain environments. Please take care of monitoring the state of * the request yourself. * * @param {String} src */ function ScriptRequest(src) { this.src = src; } var prototype = ScriptRequest.prototype; /** Sends the actual script request. * * @param {ScriptReceiver} receiver */ prototype.send = function(receiver) { var self = this; var errorString = "Error loading " + self.src; self.script = document.createElement("script"); self.script.id = receiver.id; self.script.src = self.src; self.script.type = "text/javascript"; self.script.charset = "UTF-8"; if (self.script.addEventListener) { self.script.onerror = function() { receiver.callback(errorString); }; self.script.onload = function() { receiver.callback(null); }; } else { self.script.onreadystatechange = function() { if (self.script.readyState === 'loaded' || self.script.readyState === 'complete') { receiver.callback(null); } }; } // Opera<11.6 hack for missing onerror callback if (self.script.async === undefined && document.attachEvent && /opera/i.test(navigator.userAgent)) { self.errorScript = document.createElement("script"); self.errorScript.id = receiver.id + "_error"; self.errorScript.text = receiver.name + "('" + errorString + "');"; self.script.async = self.errorScript.async = false; } else { self.script.async = true; } var head = document.getElementsByTagName('head')[0]; head.insertBefore(self.script, head.firstChild); if (self.errorScript) { head.insertBefore(self.errorScript, self.script.nextSibling); } }; /** Cleans up the DOM remains of the script request. */ prototype.cleanup = function() { if (this.script) { this.script.onload = this.script.onerror = null; this.script.onreadystatechange = null; } if (this.script && this.script.parentNode) { this.script.parentNode.removeChild(this.script); } if (this.errorScript && this.errorScript.parentNode) { this.errorScript.parentNode.removeChild(this.errorScript); } this.script = null; this.errorScript = null; }; Pusher.ScriptRequest = ScriptRequest; }).call(this); ;(function() { /** Handles loading dependency files. * * Dependency loaders don't remember whether a resource has been loaded or * not. It is caller's responsibility to make sure the resource is not loaded * twice. This is because it's impossible to detect resource loading status * without knowing its content. * * Options: * - cdn_http - url to HTTP CND * - cdn_https - url to HTTPS CDN * - version - version of pusher-js * - suffix - suffix appended to all names of dependency files * * @param {Object} options */ function DependencyLoader(options) { this.options = options; this.receivers = options.receivers || Pusher.ScriptReceivers; this.loading = {}; } var prototype = DependencyLoader.prototype; /** Loads the dependency from CDN. * * @param {String} name * @param {Function} callback */ prototype.load = function(name, callback) { var self = this; if (self.loading[name] && self.loading[name].length > 0) { self.loading[name].push(callback); } else { self.loading[name] = [callback]; var request = new Pusher.ScriptRequest(self.getPath(name)); var receiver = self.receivers.create(function(error) { self.receivers.remove(receiver); if (self.loading[name]) { var callbacks = self.loading[name]; delete self.loading[name]; var successCallback = function(wasSuccessful) { if (!wasSuccessful) { request.cleanup(); } }; for (var i = 0; i < callbacks.length; i++) { callbacks[i](error, successCallback); } } }); request.send(receiver); } }; /** Returns a root URL for pusher-js CDN. * * @returns {String} */ prototype.getRoot = function(options) { var cdn; var protocol = Pusher.Util.getDocument().location.protocol; if ((options && options.encrypted) || protocol === "https:") { cdn = this.options.cdn_https; } else { cdn = this.options.cdn_http; } // make sure there are no double slashes return cdn.replace(/\/*$/, "") + "/" + this.options.version; }; /** Returns a full path to a dependency file. * * @param {String} name * @returns {String} */ prototype.getPath = function(name, options) { return this.getRoot(options) + '/' + name + this.options.suffix + '.js'; }; Pusher.DependencyLoader = DependencyLoader; }).call(this); ;(function() { Pusher.DependenciesReceivers = new Pusher.ScriptReceiverFactory( "_pusher_dependencies", "Pusher.DependenciesReceivers" ); Pusher.Dependencies = new Pusher.DependencyLoader({ cdn_http: Pusher.cdn_http, cdn_https: Pusher.cdn_https, version: Pusher.VERSION, suffix: Pusher.dependency_suffix, receivers: Pusher.DependenciesReceivers }); function initialize() { Pusher.ready(); } // Allows calling a function when the document body is available function onDocumentBody(callback) { if (document.body) { callback(); } else { setTimeout(function() { onDocumentBody(callback); }, 0); } } function initializeOnDocumentBody() { onDocumentBody(initialize); } if (!window.JSON) { Pusher.Dependencies.load("json2", initializeOnDocumentBody); } else { initializeOnDocumentBody(); } })(); (function() { var Base64 = { encode: function (s) { return btoa(utob(s)); } }; var fromCharCode = String.fromCharCode; var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var b64tab = {}; for (var i = 0, l = b64chars.length; i < l; i++) { b64tab[b64chars.charAt(i)] = i; } var cb_utob = function(c) { var cc = c.charCodeAt(0); return cc < 0x80 ? c : cc < 0x800 ? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f)) : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) + fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) + fromCharCode(0x80 | ( cc & 0x3f)); }; var utob = function(u) { return u.replace(/[^\x00-\x7F]/g, cb_utob); }; var cb_encode = function(ccc) { var padlen = [0, 2, 1][ccc.length % 3]; var ord = ccc.charCodeAt(0) << 16 | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)); var chars = [ b64chars.charAt( ord >>> 18), b64chars.charAt((ord >>> 12) & 63), padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63), padlen >= 1 ? '=' : b64chars.charAt(ord & 63) ]; return chars.join(''); }; var btoa = window.btoa || function(b) { return b.replace(/[\s\S]{1,3}/g, cb_encode); }; Pusher.Base64 = Base64; }).call(this); (function() { /** Sends data via JSONP. * * Data is a key-value map. Its values are JSON-encoded and then passed * through base64. Finally, keys and encoded values are appended to the query * string. * * The class itself does not guarantee raising errors on failures, as it's not * possible to support such feature on all browsers. Instead, JSONP endpoint * should call back in a way that's easy to distinguish from browser calls, * for example by passing a second argument to the receiver. * * @param {String} url * @param {Object} data key-value map of data to be submitted */ function JSONPRequest(url, data) { this.url = url; this.data = data; } var prototype = JSONPRequest.prototype; /** Sends the actual JSONP request. * * @param {ScriptReceiver} receiver */ prototype.send = function(receiver) { if (this.request) { return; } var params = Pusher.Util.filterObject(this.data, function(value) { return value !== undefined; }); var query = Pusher.Util.map( Pusher.Util.flatten(encodeParamsObject(params)), Pusher.Util.method("join", "=") ).join("&"); var url = this.url + "/" + receiver.number + "?" + query; this.request = new Pusher.ScriptRequest(url); this.request.send(receiver); }; /** Cleans up the DOM remains of the JSONP request. */ prototype.cleanup = function() { if (this.request) { this.request.cleanup(); } }; function encodeParamsObject(data) { return Pusher.Util.mapObject(data, function(value) { if (typeof value === "object") { value = JSON.stringify(value); } return encodeURIComponent(Pusher.Base64.encode(value.toString())); }); } Pusher.JSONPRequest = JSONPRequest; }).call(this); (function() { function Timeline(key, session, options) { this.key = key; this.session = session; this.events = []; this.options = options || {}; this.sent = 0; this.uniqueID = 0; } var prototype = Timeline.prototype; // Log levels Timeline.ERROR = 3; Timeline.INFO = 6; Timeline.DEBUG = 7; prototype.log = function(level, event) { if (level <= this.options.level) { this.events.push( Pusher.Util.extend({}, event, { timestamp: Pusher.Util.now() }) ); if (this.options.limit && this.events.length > this.options.limit) { this.events.shift(); } } }; prototype.error = function(event) { this.log(Timeline.ERROR, event); }; prototype.info = function(event) { this.log(Timeline.INFO, event); }; prototype.debug = function(event) { this.log(Timeline.DEBUG, event); }; prototype.isEmpty = function() { return this.events.length === 0; }; prototype.send = function(sendJSONP, callback) { var self = this; var data = Pusher.Util.extend({ session: self.session, bundle: self.sent + 1, key: self.key, lib: "js", version: self.options.version, cluster: self.options.cluster, features: self.options.features, timeline: self.events }, self.options.params); self.events = []; sendJSONP(data, function(error, result) { if (!error) { self.sent++; } if (callback) { callback(error, result); } }); return true; }; prototype.generateUniqueID = function() { this.uniqueID++; return this.uniqueID; }; Pusher.Timeline = Timeline; }).call(this); (function() { function TimelineSender(timeline, options) { this.timeline = timeline; this.options = options || {}; } var prototype = TimelineSender.prototype; prototype.send = function(encrypted, callback) { var self = this; if (self.timeline.isEmpty()) { return; } var sendJSONP = function(data, callback) { var scheme = "http" + (encrypted ? "s" : "") + "://"; var url = scheme + (self.host || self.options.host) + self.options.path; var request = new Pusher.JSONPRequest(url, data); var receiver = Pusher.ScriptReceivers.create(function(error, result) { Pusher.ScriptReceivers.remove(receiver); request.cleanup(); if (result && result.host) { self.host = result.host; } if (callback) { callback(error, result); } }); request.send(receiver); }; self.timeline.send(sendJSONP, callback); }; Pusher.TimelineSender = TimelineSender; }).call(this); ;(function() { /** Launches all substrategies and emits prioritized connected transports. * * @param {Array} strategies */ function BestConnectedEverStrategy(strategies) { this.strategies = strategies; } var prototype = BestConnectedEverStrategy.prototype; prototype.isSupported = function() { return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); }; prototype.connect = function(minPriority, callback) { return connect(this.strategies, minPriority, function(i, runners) { return function(error, handshake) { runners[i].error = error; if (error) { if (allRunnersFailed(runners)) { callback(true); } return; } Pusher.Util.apply(runners, function(runner) { runner.forceMinPriority(handshake.transport.priority); }); callback(null, handshake); }; }); }; /** Connects to all strategies in parallel. * * Callback builder should be a function that takes two arguments: index * and a list of runners. It should return another function that will be * passed to the substrategy with given index. Runners can be aborted using * abortRunner(s) functions from this class. * * @param {Array} strategies * @param {Function} callbackBuilder * @return {Object} strategy runner */ function connect(strategies, minPriority, callbackBuilder) { var runners = Pusher.Util.map(strategies, function(strategy, i, _, rs) { return strategy.connect(minPriority, callbackBuilder(i, rs)); }); return { abort: function() { Pusher.Util.apply(runners, abortRunner); }, forceMinPriority: function(p) { Pusher.Util.apply(runners, function(runner) { runner.forceMinPriority(p); }); } }; } function allRunnersFailed(runners) { return Pusher.Util.all(runners, function(runner) { return Boolean(runner.error); }); } function abortRunner(runner) { if (!runner.error && !runner.aborted) { runner.abort(); runner.aborted = true; } } Pusher.BestConnectedEverStrategy = BestConnectedEverStrategy; }).call(this); ;(function() { /** Caches last successful transport and uses it for following attempts. * * @param {Strategy} strategy * @param {Object} transports * @param {Object} options */ function CachedStrategy(strategy, transports, options) { this.strategy = strategy; this.transports = transports; this.ttl = options.ttl || 1800*1000; this.encrypted = options.encrypted; this.timeline = options.timeline; } var prototype = CachedStrategy.prototype; prototype.isSupported = function() { return this.strategy.isSupported(); }; prototype.connect = function(minPriority, callback) { var encrypted = this.encrypted; var info = fetchTransportCache(encrypted); var strategies = [this.strategy]; if (info && info.timestamp + this.ttl >= Pusher.Util.now()) { var transport = this.transports[info.transport]; if (transport) { this.timeline.info({ cached: true, transport: info.transport, latency: info.latency }); strategies.push(new Pusher.SequentialStrategy([transport], { timeout: info.latency * 2 + 1000, failFast: true })); } } var startTimestamp = Pusher.Util.now(); var runner = strategies.pop().connect( minPriority, function cb(error, handshake) { if (error) { flushTransportCache(encrypted); if (strategies.length > 0) { startTimestamp = Pusher.Util.now(); runner = strategies.pop().connect(minPriority, cb); } else { callback(error); } } else { storeTransportCache( encrypted, handshake.transport.name, Pusher.Util.now() - startTimestamp ); callback(null, handshake); } } ); return { abort: function() { runner.abort(); }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; }; function getTransportCacheKey(encrypted) { return "pusherTransport" + (encrypted ? "Encrypted" : "Unencrypted"); } function fetchTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { var serializedCache = storage[getTransportCacheKey(encrypted)]; if (serializedCache) { return JSON.parse(serializedCache); } } catch (e) { flushTransportCache(encrypted); } } return null; } function storeTransportCache(encrypted, transport, latency) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { storage[getTransportCacheKey(encrypted)] = JSON.stringify({ timestamp: Pusher.Util.now(), transport: transport, latency: latency }); } catch (e) { // catch over quota exceptions raised by localStorage } } } function flushTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { delete storage[getTransportCacheKey(encrypted)]; } catch (e) { // catch exceptions raised by localStorage } } } Pusher.CachedStrategy = CachedStrategy; }).call(this); ;(function() { /** Runs substrategy after specified delay. * * Options: * - delay - time in miliseconds to delay the substrategy attempt * * @param {Strategy} strategy * @param {Object} options */ function DelayedStrategy(strategy, options) { this.strategy = strategy; this.options = { delay: options.delay }; } var prototype = DelayedStrategy.prototype; prototype.isSupported = function() { return this.strategy.isSupported(); }; prototype.connect = function(minPriority, callback) { var strategy = this.strategy; var runner; var timer = new Pusher.Timer(this.options.delay, function() { runner = strategy.connect(minPriority, callback); }); return { abort: function() { timer.ensureAborted(); if (runner) { runner.abort(); } }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; }; Pusher.DelayedStrategy = DelayedStrategy; }).call(this); ;(function() { /** Launches the substrategy and terminates on the first open connection. * * @param {Strategy} strategy */ function FirstConnectedStrategy(strategy) { this.strategy = strategy; } var prototype = FirstConnectedStrategy.prototype; prototype.isSupported = function() { return this.strategy.isSupported(); }; prototype.connect = function(minPriority, callback) { var runner = this.strategy.connect( minPriority, function(error, handshake) { if (handshake) { runner.abort(); } callback(error, handshake); } ); return runner; }; Pusher.FirstConnectedStrategy = FirstConnectedStrategy; }).call(this); ;(function() { /** Proxies method calls to one of substrategies basing on the test function. * * @param {Function} test * @param {Strategy} trueBranch strategy used when test returns true * @param {Strategy} falseBranch strategy used when test returns false */ function IfStrategy(test, trueBranch, falseBranch) { this.test = test; this.trueBranch = trueBranch; this.falseBranch = falseBranch; } var prototype = IfStrategy.prototype; prototype.isSupported = function() { var branch = this.test() ? this.trueBranch : this.falseBranch; return branch.isSupported(); }; prototype.connect = function(minPriority, callback) { var branch = this.test() ? this.trueBranch : this.falseBranch; return branch.connect(minPriority, callback); }; Pusher.IfStrategy = IfStrategy; }).call(this); ;(function() { /** Loops through strategies with optional timeouts. * * Options: * - loop - whether it should loop through the substrategy list * - timeout - initial timeout for a single substrategy * - timeoutLimit - maximum timeout * * @param {Strategy[]} strategies * @param {Object} options */ function SequentialStrategy(strategies, options) { this.strategies = strategies; this.loop = Boolean(options.loop); this.failFast = Boolean(options.failFast); this.timeout = options.timeout; this.timeoutLimit = options.timeoutLimit; } var prototype = SequentialStrategy.prototype; prototype.isSupported = function() { return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported")); }; prototype.connect = function(minPriority, callback) { var self = this; var strategies = this.strategies; var current = 0; var timeout = this.timeout; var runner = null; var tryNextStrategy = function(error, handshake) { if (handshake) { callback(null, handshake); } else { current = current + 1; if (self.loop) { current = current % strategies.length; } if (current < strategies.length) { if (timeout) { timeout = timeout * 2; if (self.timeoutLimit) { timeout = Math.min(timeout, self.timeoutLimit); } } runner = self.tryStrategy( strategies[current], minPriority, { timeout: timeout, failFast: self.failFast }, tryNextStrategy ); } else { callback(true); } } }; runner = this.tryStrategy( strategies[current], minPriority, { timeout: timeout, failFast: this.failFast }, tryNextStrategy ); return { abort: function() { runner.abort(); }, forceMinPriority: function(p) { minPriority = p; if (runner) { runner.forceMinPriority(p); } } }; }; /** @private */ prototype.tryStrategy = function(strategy, minPriority, options, callback) { var timer = null; var runner = null; if (options.timeout > 0) { timer = new Pusher.Timer(options.timeout, function() { runner.abort(); callback(true); }); } runner = strategy.connect(minPriority, function(error, handshake) { if (error && timer && timer.isRunning() && !options.failFast) { // advance to the next strategy after the timeout return; } if (timer) { timer.ensureAborted(); } callback(error, handshake); }); return { abort: function() { if (timer) { timer.ensureAborted(); } runner.abort(); }, forceMinPriority: function(p) { runner.forceMinPriority(p); } }; }; Pusher.SequentialStrategy = SequentialStrategy; }).call(this); ;(function() { /** Provides a strategy interface for transports. * * @param {String} name * @param {Number} priority * @param {Class} transport * @param {Object} options */ function TransportStrategy(name, priority, transport, options) { this.name = name; this.priority = priority; this.transport = transport; this.options = options || {}; } var prototype = TransportStrategy.prototype; /** Returns whether the transport is supported in the browser. * * @returns {Boolean} */ prototype.isSupported = function() { return this.transport.isSupported({ encrypted: this.options.encrypted }); }; /** Launches a connection attempt and returns a strategy runner. * * @param {Function} callback * @return {Object} strategy runner */ prototype.connect = function(minPriority, callback) { if (!this.isSupported()) { return failAttempt(new Pusher.Errors.UnsupportedStrategy(), callback); } else if (this.priority < minPriority) { return failAttempt(new Pusher.Errors.TransportPriorityTooLow(), callback); } var self = this; var connected = false; var transport = this.transport.createConnection( this.name, this.priority, this.options.key, this.options ); var handshake = null; var onInitialized = function() { transport.unbind("initialized", onInitialized); transport.connect(); }; var onOpen = function() { handshake = new Pusher.Handshake(transport, function(result) { connected = true; unbindListeners(); callback(null, result); }); }; var onError = function(error) { unbindListeners(); callback(error); }; var onClosed = function() { unbindListeners(); callback(new Pusher.Errors.TransportClosed(transport)); }; var unbindListeners = function() { transport.unbind("initialized", onInitialized); transport.unbind("open", onOpen); transport.unbind("error", onError); transport.unbind("closed", onClosed); }; transport.bind("initialized", onInitialized); transport.bind("open", onOpen); transport.bind("error", onError); transport.bind("closed", onClosed); // connect will be called automatically after initialization transport.initialize(); return { abort: function() { if (connected) { return; } unbindListeners(); if (handshake) { handshake.close(); } else { transport.close(); } }, forceMinPriority: function(p) { if (connected) { return; } if (self.priority < p) { if (handshake) { handshake.close(); } else { transport.close(); } } } }; }; function failAttempt(error, callback) { Pusher.Util.defer(function() { callback(error); }); return { abort: function() {}, forceMinPriority: function() {} }; } Pusher.TransportStrategy = TransportStrategy; }).call(this); (function() { function getGenericURL(baseScheme, params, path) { var scheme = baseScheme + (params.encrypted ? "s" : ""); var host = params.encrypted ? params.hostEncrypted : params.hostUnencrypted; return scheme + "://" + host + path; } function getGenericPath(key, queryString) { var path = "/app/" + key; var query = "?protocol=" + Pusher.PROTOCOL + "&client=js" + "&version=" + Pusher.VERSION + (queryString ? ("&" + queryString) : ""); return path + query; } /** URL schemes for different transport types. */ Pusher.URLSchemes = { /** Standard WebSocket URL scheme. */ ws: { getInitial: function(key, params) { return getGenericURL("ws", params, getGenericPath(key, "flash=false")); } }, /** URL scheme for Flash. Same as WebSocket, but with a flash parameter. */ flash: { getInitial: function(key, params) { return getGenericURL("ws", params, getGenericPath(key, "flash=true")); } }, /** SockJS URL scheme. Supplies the path separately from the initial URL. */ sockjs: { getInitial: function(key, params) { return getGenericURL("http", params, params.httpPath || "/pusher", ""); }, getPath: function(key, params) { return getGenericPath(key); } }, /** URL scheme for HTTP transports. Basically, WS scheme with a prefix. */ http: { getInitial: function(key, params) { var path = (params.httpPath || "/pusher") + getGenericPath(key); return getGenericURL("http", params, path); } } }; }).call(this); (function() { /** Provides universal API for transport connections. * * Transport connection is a low-level object that wraps a connection method * and exposes a simple evented interface for the connection state and * messaging. It does not implement Pusher-specific WebSocket protocol. * * Additionally, it fetches resources needed for transport to work and exposes * an interface for querying transport features. * * States: * - new - initial state after constructing the object * - initializing - during initialization phase, usually fetching resources * - intialized - ready to establish a connection * - connection - when connection is being established * - open - when connection ready to be used * - closed - after connection was closed be either side * * Emits: * - error - after the connection raised an error * * Options: * - encrypted - whether connection should use ssl * - hostEncrypted - host to connect to when connection is encrypted * - hostUnencrypted - host to connect to when connection is not encrypted * * @param {String} key application key * @param {Object} options */ function TransportConnection(hooks, name, priority, key, options) { Pusher.EventsDispatcher.call(this); this.hooks = hooks; this.name = name; this.priority = priority; this.key = key; this.options = options; this.state = "new"; this.timeline = options.timeline; this.activityTimeout = options.activityTimeout; this.id = this.timeline.generateUniqueID(); } var prototype = TransportConnection.prototype; Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); /** Checks whether the transport handles activity checks by itself. * * @return {Boolean} */ prototype.handlesActivityChecks = function() { return Boolean(this.hooks.handlesActivityChecks); }; /** Checks whether the transport supports the ping/pong API. * * @return {Boolean} */ prototype.supportsPing = function() { return Boolean(this.hooks.supportsPing); }; /** Initializes the transport. * * Fetches resources if needed and then transitions to initialized. */ prototype.initialize = function() { var self = this; self.timeline.info(self.buildTimelineMessage({ transport: self.name + (self.options.encrypted ? "s" : "") })); if (self.hooks.beforeInitialize) { self.hooks.beforeInitialize(); } if (self.hooks.isInitialized()) { self.changeState("initialized"); } else if (self.hooks.file) { self.changeState("initializing"); Pusher.Dependencies.load(self.hooks.file, function(error, callback) { if (self.hooks.isInitialized()) { self.changeState("initialized"); callback(true); } else { if (error) { self.onError(error); } self.onClose(); callback(false); } }); } else { self.onClose(); } }; /** Tries to establish a connection. * * @returns {Boolean} false if transport is in invalid state */ prototype.connect = function() { var self = this; if (self.socket || self.state !== "initialized") { return false; } var url = self.hooks.urls.getInitial(self.key, self.options); try { self.socket = self.hooks.getSocket(url, self.options); } catch (e) { Pusher.Util.defer(function() { self.onError(e); self.changeState("closed"); }); return false; } self.bindListeners(); Pusher.debug("Connecting", { transport: self.name, url: url }); self.changeState("connecting"); return true; }; /** Closes the connection. * * @return {Boolean} true if there was a connection to close */ prototype.close = function() { if (this.socket) { this.socket.close(); return true; } else { return false; } }; /** Sends data over the open connection. * * @param {String} data * @return {Boolean} true only when in the "open" state */ prototype.send = function(data) { var self = this; if (self.state === "open") { // Workaround for MobileSafari bug (see https://gist.github.com/2052006) Pusher.Util.defer(function() { if (self.socket) { self.socket.send(data); } }); return true; } else { return false; } }; /** Sends a ping if the connection is open and transport supports it. */ prototype.ping = function() { if (this.state === "open" && this.supportsPing()) { this.socket.ping(); } }; /** @private */ prototype.onOpen = function() { if (this.hooks.beforeOpen) { this.hooks.beforeOpen( this.socket, this.hooks.urls.getPath(this.key, this.options) ); } this.changeState("open"); this.socket.onopen = undefined; }; /** @private */ prototype.onError = function(error) { this.emit("error", { type: 'WebSocketError', error: error }); this.timeline.error(this.buildTimelineMessage({ error: error.toString() })); }; /** @private */ prototype.onClose = function(closeEvent) { if (closeEvent) { this.changeState("closed", { code: closeEvent.code, reason: closeEvent.reason, wasClean: closeEvent.wasClean }); } else { this.changeState("closed"); } this.unbindListeners(); this.socket = undefined; }; /** @private */ prototype.onMessage = function(message) { this.emit("message", message); }; /** @private */ prototype.onActivity = function() { this.emit("activity"); }; /** @private */ prototype.bindListeners = function() { var self = this; self.socket.onopen = function() { self.onOpen(); }; self.socket.onerror = function(error) { self.onError(error); }; self.socket.onclose = function(closeEvent) { self.onClose(closeEvent); }; self.socket.onmessage = function(message) { self.onMessage(message); }; if (self.supportsPing()) { self.socket.onactivity = function() { self.onActivity(); }; } }; /** @private */ prototype.unbindListeners = function() { if (this.socket) { this.socket.onopen = undefined; this.socket.onerror = undefined; this.socket.onclose = undefined; this.socket.onmessage = undefined; if (this.supportsPing()) { this.socket.onactivity = undefined; } } }; /** @private */ prototype.changeState = function(state, params) { this.state = state; this.timeline.info(this.buildTimelineMessage({ state: state, params: params })); this.emit(state, params); }; /** @private */ prototype.buildTimelineMessage = function(message) { return Pusher.Util.extend({ cid: this.id }, message); }; Pusher.TransportConnection = TransportConnection; }).call(this); (function() { /** Provides interface for transport connection instantiation. * * Takes transport-specific hooks as the only argument, which allow checking * for transport support and creating its connections. * * Supported hooks: * - file - the name of the file to be fetched during initialization * - urls - URL scheme to be used by transport * - handlesActivityCheck - true when the transport handles activity checks * - supportsPing - true when the transport has a ping/activity API * - isSupported - tells whether the transport is supported in the environment * - getSocket - creates a WebSocket-compatible transport socket * * See transports.js for specific implementations. * * @param {Object} hooks object containing all needed transport hooks */ function Transport(hooks) { this.hooks = hooks; } var prototype = Transport.prototype; /** Returns whether the transport is supported in the environment. * * @param {Object} environment the environment details (encryption, settings) * @returns {Boolean} true when the transport is supported */ prototype.isSupported = function(environment) { return this.hooks.isSupported(environment); }; /** Creates a transport connection. * * @param {String} name * @param {Number} priority * @param {String} key the application key * @param {Object} options * @returns {TransportConnection} */ prototype.createConnection = function(name, priority, key, options) { return new Pusher.TransportConnection( this.hooks, name, priority, key, options ); }; Pusher.Transport = Transport; }).call(this); (function() { /** WebSocket transport. * * Uses native WebSocket implementation, including MozWebSocket supported by * earlier Firefox versions. */ Pusher.WSTransport = new Pusher.Transport({ urls: Pusher.URLSchemes.ws, handlesActivityChecks: false, supportsPing: false, isInitialized: function() { return Boolean(window.WebSocket || window.MozWebSocket); }, isSupported: function() { return Boolean(window.WebSocket || window.MozWebSocket); }, getSocket: function(url) { var Constructor = window.WebSocket || window.MozWebSocket; return new Constructor(url); } }); /** Flash transport using the WebSocket protocol. */ Pusher.FlashTransport = new Pusher.Transport({ file: "flashfallback", urls: Pusher.URLSchemes.flash, handlesActivityChecks: false, supportsPing: false, isSupported: function() { try { return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); } catch (e1) { try { var nav = Pusher.Util.getNavigator(); return Boolean( nav && nav.mimeTypes && nav.mimeTypes["application/x-shockwave-flash"] !== undefined ); } catch (e2) { return false; } } }, beforeInitialize: function() { if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) { window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; } window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() + "/WebSocketMain.swf"; }, isInitialized: function() { return window.FlashWebSocket !== undefined; }, getSocket: function(url) { return new FlashWebSocket(url); } }); /** SockJS transport. */ Pusher.SockJSTransport = new Pusher.Transport({ file: "sockjs", urls: Pusher.URLSchemes.sockjs, handlesActivityChecks: true, supportsPing: false, isSupported: function() { return true; }, isInitialized: function() { return window.SockJS !== undefined; }, getSocket: function(url, options) { return new SockJS(url, null, { js_path: Pusher.Dependencies.getPath("sockjs", { encrypted: options.encrypted }), ignore_null_origin: options.ignoreNullOrigin }); }, beforeOpen: function(socket, path) { socket.send(JSON.stringify({ path: path })); } }); var httpConfiguration = { urls: Pusher.URLSchemes.http, handlesActivityChecks: false, supportsPing: true, isInitialized: function() { return Boolean(Pusher.HTTP.Socket); } }; var streamingConfiguration = Pusher.Util.extend( { getSocket: function(url) { return Pusher.HTTP.getStreamingSocket(url); } }, httpConfiguration ); var pollingConfiguration = Pusher.Util.extend( { getSocket: function(url) { return Pusher.HTTP.getPollingSocket(url); } }, httpConfiguration ); var xhrConfiguration = { file: "xhr", isSupported: Pusher.Util.isXHRSupported }; var xdrConfiguration = { file: "xdr", isSupported: function(environment) { return Pusher.Util.isXDRSupported(environment.encrypted); } }; /** HTTP streaming transport using CORS-enabled XMLHttpRequest. */ Pusher.XHRStreamingTransport = new Pusher.Transport( Pusher.Util.extend({}, streamingConfiguration, xhrConfiguration) ); /** HTTP streaming transport using XDomainRequest (IE 8,9). */ Pusher.XDRStreamingTransport = new Pusher.Transport( Pusher.Util.extend({}, streamingConfiguration, xdrConfiguration) ); /** HTTP long-polling transport using CORS-enabled XMLHttpRequest. */ Pusher.XHRPollingTransport = new Pusher.Transport( Pusher.Util.extend({}, pollingConfiguration, xhrConfiguration) ); /** HTTP long-polling transport using XDomainRequest (IE 8,9). */ Pusher.XDRPollingTransport = new Pusher.Transport( Pusher.Util.extend({}, pollingConfiguration, xdrConfiguration) ); }).call(this); ;(function() { /** Creates transport connections monitored by a transport manager. * * When a transport is closed, it might mean the environment does not support * it. It's possible that messages get stuck in an intermediate buffer or * proxies terminate inactive connections. To combat these problems, * assistants monitor the connection lifetime, report unclean exits and * adjust ping timeouts to keep the connection active. The decision to disable * a transport is the manager's responsibility. * * @param {TransportManager} manager * @param {TransportConnection} transport * @param {Object} options */ function AssistantToTheTransportManager(manager, transport, options) { this.manager = manager; this.transport = transport; this.minPingDelay = options.minPingDelay; this.maxPingDelay = options.maxPingDelay; this.pingDelay = undefined; } var prototype = AssistantToTheTransportManager.prototype; /** Creates a transport connection. * * This function has the same API as Transport#createConnection. * * @param {String} name * @param {Number} priority * @param {String} key the application key * @param {Object} options * @returns {TransportConnection} */ prototype.createConnection = function(name, priority, key, options) { var self = this; options = Pusher.Util.extend({}, options, { activityTimeout: self.pingDelay }); var connection = self.transport.createConnection( name, priority, key, options ); var openTimestamp = null; var onOpen = function() { connection.unbind("open", onOpen); connection.bind("closed", onClosed); openTimestamp = Pusher.Util.now(); }; var onClosed = function(closeEvent) { connection.unbind("closed", onClosed); if (closeEvent.code === 1002 || closeEvent.code === 1003) { // we don't want to use transports not obeying the protocol self.manager.reportDeath(); } else if (!closeEvent.wasClean && openTimestamp) { // report deaths only for short-living transport var lifespan = Pusher.Util.now() - openTimestamp; if (lifespan < 2 * self.maxPingDelay) { self.manager.reportDeath(); self.pingDelay = Math.max(lifespan / 2, self.minPingDelay); } } }; connection.bind("open", onOpen); return connection; }; /** Returns whether the transport is supported in the environment. * * This function has the same API as Transport#isSupported. Might return false * when the manager decides to kill the transport. * * @param {Object} environment the environment details (encryption, settings) * @returns {Boolean} true when the transport is supported */ prototype.isSupported = function(environment) { return this.manager.isAlive() && this.transport.isSupported(environment); }; Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager; }).call(this); ;(function() { /** Keeps track of the number of lives left for a transport. * * In the beginning of a session, transports may be assigned a number of * lives. When an AssistantToTheTransportManager instance reports a transport * connection closed uncleanly, the transport loses a life. When the number * of lives drops to zero, the transport gets disabled by its manager. * * @param {Object} options */ function TransportManager(options) { this.options = options || {}; this.livesLeft = this.options.lives || Infinity; } var prototype = TransportManager.prototype; /** Creates a assistant for the transport. * * @param {Transport} transport * @returns {AssistantToTheTransportManager} */ prototype.getAssistant = function(transport) { return new Pusher.AssistantToTheTransportManager(this, transport, { minPingDelay: this.options.minPingDelay, maxPingDelay: this.options.maxPingDelay }); }; /** Returns whether the transport has any lives left. * * @returns {Boolean} */ prototype.isAlive = function() { return this.livesLeft > 0; }; /** Takes one life from the transport. */ prototype.reportDeath = function() { this.livesLeft -= 1; }; Pusher.TransportManager = TransportManager; }).call(this); ;(function() { var StrategyBuilder = { /** Transforms a JSON scheme to a strategy tree. * * @param {Array} scheme JSON strategy scheme * @param {Object} options a hash of symbols to be included in the scheme * @returns {Strategy} strategy tree that's represented by the scheme */ build: function(scheme, options) { var context = Pusher.Util.extend({}, globalContext, options); return evaluate(scheme, context)[1].strategy; } }; var transports = { ws: Pusher.WSTransport, flash: Pusher.FlashTransport, sockjs: Pusher.SockJSTransport, xhr_streaming: Pusher.XHRStreamingTransport, xdr_streaming: Pusher.XDRStreamingTransport, xhr_polling: Pusher.XHRPollingTransport, xdr_polling: Pusher.XDRPollingTransport }; var UnsupportedStrategy = { isSupported: function() { return false; }, connect: function(_, callback) { var deferred = Pusher.Util.defer(function() { callback(new Pusher.Errors.UnsupportedStrategy()); }); return { abort: function() { deferred.ensureAborted(); }, forceMinPriority: function() {} }; } }; // DSL bindings function returnWithOriginalContext(f) { return function(context) { return [f.apply(this, arguments), context]; }; } var globalContext = { extend: function(context, first, second) { return [Pusher.Util.extend({}, first, second), context]; }, def: function(context, name, value) { if (context[name] !== undefined) { throw "Redefining symbol " + name; } context[name] = value; return [undefined, context]; }, def_transport: function(context, name, type, priority, options, manager) { var transportClass = transports[type]; if (!transportClass) { throw new Pusher.Errors.UnsupportedTransport(type); } var enabled = (!context.enabledTransports || Pusher.Util.arrayIndexOf(context.enabledTransports, name) !== -1) && (!context.disabledTransports || Pusher.Util.arrayIndexOf(context.disabledTransports, name) === -1) && (name !== "flash" || context.disableFlash !== true); var transport; if (enabled) { transport = new Pusher.TransportStrategy( name, priority, manager ? manager.getAssistant(transportClass) : transportClass, Pusher.Util.extend({ key: context.key, encrypted: context.encrypted, timeline: context.timeline, ignoreNullOrigin: context.ignoreNullOrigin }, options) ); } else { transport = UnsupportedStrategy; } var newContext = context.def(context, name, transport)[1]; newContext.transports = context.transports || {}; newContext.transports[name] = transport; return [undefined, newContext]; }, transport_manager: returnWithOriginalContext(function(_, options) { return new Pusher.TransportManager(options); }), sequential: returnWithOriginalContext(function(_, options) { var strategies = Array.prototype.slice.call(arguments, 2); return new Pusher.SequentialStrategy(strategies, options); }), cached: returnWithOriginalContext(function(context, ttl, strategy){ return new Pusher.CachedStrategy(strategy, context.transports, { ttl: ttl, timeline: context.timeline, encrypted: context.encrypted }); }), first_connected: returnWithOriginalContext(function(_, strategy) { return new Pusher.FirstConnectedStrategy(strategy); }), best_connected_ever: returnWithOriginalContext(function() { var strategies = Array.prototype.slice.call(arguments, 1); return new Pusher.BestConnectedEverStrategy(strategies); }), delayed: returnWithOriginalContext(function(_, delay, strategy) { return new Pusher.DelayedStrategy(strategy, { delay: delay }); }), "if": returnWithOriginalContext(function(_, test, trueBranch, falseBranch) { return new Pusher.IfStrategy(test, trueBranch, falseBranch); }), is_supported: returnWithOriginalContext(function(_, strategy) { return function() { return strategy.isSupported(); }; }) }; // DSL interpreter function isSymbol(expression) { return (typeof expression === "string") && expression.charAt(0) === ":"; } function getSymbolValue(expression, context) { return context[expression.slice(1)]; } function evaluateListOfExpressions(expressions, context) { if (expressions.length === 0) { return [[], context]; } var head = evaluate(expressions[0], context); var tail = evaluateListOfExpressions(expressions.slice(1), head[1]); return [[head[0]].concat(tail[0]), tail[1]]; } function evaluateString(expression, context) { if (!isSymbol(expression)) { return [expression, context]; } var value = getSymbolValue(expression, context); if (value === undefined) { throw "Undefined symbol " + expression; } return [value, context]; } function evaluateArray(expression, context) { if (isSymbol(expression[0])) { var f = getSymbolValue(expression[0], context); if (expression.length > 1) { if (typeof f !== "function") { throw "Calling non-function " + expression[0]; } var args = [Pusher.Util.extend({}, context)].concat( Pusher.Util.map(expression.slice(1), function(arg) { return evaluate(arg, Pusher.Util.extend({}, context))[0]; }) ); return f.apply(this, args); } else { return [f, context]; } } else { return evaluateListOfExpressions(expression, context); } } function evaluate(expression, context) { var expressionType = typeof expression; if (typeof expression === "string") { return evaluateString(expression, context); } else if (typeof expression === "object") { if (expression instanceof Array && expression.length > 0) { return evaluateArray(expression, context); } } return [expression, context]; } Pusher.StrategyBuilder = StrategyBuilder; }).call(this); ;(function() { /** * Provides functions for handling Pusher protocol-specific messages. */ var Protocol = {}; /** * Decodes a message in a Pusher format. * * Throws errors when messages are not parse'able. * * @param {Object} message * @return {Object} */ Protocol.decodeMessage = function(message) { try { var params = JSON.parse(message.data); if (typeof params.data === 'string') { try { params.data = JSON.parse(params.data); } catch (e) { if (!(e instanceof SyntaxError)) { // TODO looks like unreachable code // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/JSON/parse throw e; } } } return params; } catch (e) { throw { type: 'MessageParseError', error: e, data: message.data}; } }; /** * Encodes a message to be sent. * * @param {Object} message * @return {String} */ Protocol.encodeMessage = function(message) { return JSON.stringify(message); }; /** Processes a handshake message and returns appropriate actions. * * Returns an object with an 'action' and other action-specific properties. * * There are three outcomes when calling this function. First is a successful * connection attempt, when pusher:connection_established is received, which * results in a 'connected' action with an 'id' property. When passed a * pusher:error event, it returns a result with action appropriate to the * close code and an error. Otherwise, it raises an exception. * * @param {String} message * @result Object */ Protocol.processHandshake = function(message) { message = this.decodeMessage(message); if (message.event === "pusher:connection_established") { if (!message.data.activity_timeout) { throw "No activity timeout specified in handshake"; } return { action: "connected", id: message.data.socket_id, activityTimeout: message.data.activity_timeout * 1000 }; } else if (message.event === "pusher:error") { // From protocol 6 close codes are sent only once, so this only // happens when connection does not support close codes return { action: this.getCloseAction(message.data), error: this.getCloseError(message.data) }; } else { throw "Invalid handshake"; } }; /** * Dispatches the close event and returns an appropriate action name. * * See: * 1. https://developer.mozilla.org/en-US/docs/WebSockets/WebSockets_reference/CloseEvent * 2. http://pusher.com/docs/pusher_protocol * * @param {CloseEvent} closeEvent * @return {String} close action name */ Protocol.getCloseAction = function(closeEvent) { if (closeEvent.code < 4000) { // ignore 1000 CLOSE_NORMAL, 1001 CLOSE_GOING_AWAY, // 1005 CLOSE_NO_STATUS, 1006 CLOSE_ABNORMAL // ignore 1007...3999 // handle 1002 CLOSE_PROTOCOL_ERROR, 1003 CLOSE_UNSUPPORTED, // 1004 CLOSE_TOO_LARGE if (closeEvent.code >= 1002 && closeEvent.code <= 1004) { return "backoff"; } else { return null; } } else if (closeEvent.code === 4000) { return "ssl_only"; } else if (closeEvent.code < 4100) { return "refused"; } else if (closeEvent.code < 4200) { return "backoff"; } else if (closeEvent.code < 4300) { return "retry"; } else { // unknown error return "refused"; } }; /** * Returns an error or null basing on the close event. * * Null is returned when connection was closed cleanly. Otherwise, an object * with error details is returned. * * @param {CloseEvent} closeEvent * @return {Object} error object */ Protocol.getCloseError = function(closeEvent) { if (closeEvent.code !== 1000 && closeEvent.code !== 1001) { return { type: 'PusherError', data: { code: closeEvent.code, message: closeEvent.reason || closeEvent.message } }; } else { return null; } }; Pusher.Protocol = Protocol; }).call(this); ;(function() { /** * Provides Pusher protocol interface for transports. * * Emits following events: * - message - on received messages * - ping - on ping requests * - pong - on pong responses * - error - when the transport emits an error * - closed - after closing the transport * * It also emits more events when connection closes with a code. * See Protocol.getCloseAction to get more details. * * @param {Number} id * @param {AbstractTransport} transport */ function Connection(id, transport) { Pusher.EventsDispatcher.call(this); this.id = id; this.transport = transport; this.activityTimeout = transport.activityTimeout; this.bindListeners(); } var prototype = Connection.prototype; Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); /** Returns whether used transport handles activity checks by itself * * @returns {Boolean} true if activity checks are handled by the transport */ prototype.handlesActivityChecks = function() { return this.transport.handlesActivityChecks(); }; /** Sends raw data. * * @param {String} data */ prototype.send = function(data) { return this.transport.send(data); }; /** Sends an event. * * @param {String} name * @param {String} data * @param {String} [channel] * @returns {Boolean} whether message was sent or not */ prototype.send_event = function(name, data, channel) { var message = { event: name, data: data }; if (channel) { message.channel = channel; } Pusher.debug('Event sent', message); return this.send(Pusher.Protocol.encodeMessage(message)); }; /** Sends a ping message to the server. * * Basing on the underlying transport, it might send either transport's * protocol-specific ping or pusher:ping event. */ prototype.ping = function() { if (this.transport.supportsPing()) { this.transport.ping(); } else { this.send_event('pusher:ping', {}); } }; /** Closes the connection. */ prototype.close = function() { this.transport.close(); }; /** @private */ prototype.bindListeners = function() { var self = this; var listeners = { message: function(m) { var message; try { message = Pusher.Protocol.decodeMessage(m); } catch(e) { self.emit('error', { type: 'MessageParseError', error: e, data: m.data }); } if (message !== undefined) { Pusher.debug('Event recd', message); switch (message.event) { case 'pusher:error': self.emit('error', { type: 'PusherError', data: message.data }); break; case 'pusher:ping': self.emit("ping"); break; case 'pusher:pong': self.emit("pong"); break; } self.emit('message', message); } }, activity: function() { self.emit("activity"); }, error: function(error) { self.emit("error", { type: "WebSocketError", error: error }); }, closed: function(closeEvent) { unbindListeners(); if (closeEvent && closeEvent.code) { self.handleCloseEvent(closeEvent); } self.transport = null; self.emit("closed"); } }; var unbindListeners = function() { Pusher.Util.objectApply(listeners, function(listener, event) { self.transport.unbind(event, listener); }); }; Pusher.Util.objectApply(listeners, function(listener, event) { self.transport.bind(event, listener); }); }; /** @private */ prototype.handleCloseEvent = function(closeEvent) { var action = Pusher.Protocol.getCloseAction(closeEvent); var error = Pusher.Protocol.getCloseError(closeEvent); if (error) { this.emit('error', error); } if (action) { this.emit(action); } }; Pusher.Connection = Connection; }).call(this); ;(function() { /** * Handles Pusher protocol handshakes for transports. * * Calls back with a result object after handshake is completed. Results * always have two fields: * - action - string describing action to be taken after the handshake * - transport - the transport object passed to the constructor * * Different actions can set different additional properties on the result. * In the case of 'connected' action, there will be a 'connection' property * containing a Connection object for the transport. Other actions should * carry an 'error' property. * * @param {AbstractTransport} transport * @param {Function} callback */ function Handshake(transport, callback) { this.transport = transport; this.callback = callback; this.bindListeners(); } var prototype = Handshake.prototype; prototype.close = function() { this.unbindListeners(); this.transport.close(); }; /** @private */ prototype.bindListeners = function() { var self = this; self.onMessage = function(m) { self.unbindListeners(); try { var result = Pusher.Protocol.processHandshake(m); if (result.action === "connected") { self.finish("connected", { connection: new Pusher.Connection(result.id, self.transport), activityTimeout: result.activityTimeout }); } else { self.finish(result.action, { error: result.error }); self.transport.close(); } } catch (e) { self.finish("error", { error: e }); self.transport.close(); } }; self.onClosed = function(closeEvent) { self.unbindListeners(); var action = Pusher.Protocol.getCloseAction(closeEvent) || "backoff"; var error = Pusher.Protocol.getCloseError(closeEvent); self.finish(action, { error: error }); }; self.transport.bind("message", self.onMessage); self.transport.bind("closed", self.onClosed); }; /** @private */ prototype.unbindListeners = function() { this.transport.unbind("message", this.onMessage); this.transport.unbind("closed", this.onClosed); }; /** @private */ prototype.finish = function(action, params) { this.callback( Pusher.Util.extend({ transport: this.transport, action: action }, params) ); }; Pusher.Handshake = Handshake; }).call(this); ;(function() { /** Manages connection to Pusher. * * Uses a strategy (currently only default), timers and network availability * info to establish a connection and export its state. In case of failures, * manages reconnection attempts. * * Exports state changes as following events: * - "state_change", { previous: p, current: state } * - state * * States: * - initialized - initial state, never transitioned to * - connecting - connection is being established * - connected - connection has been fully established * - disconnected - on requested disconnection * - unavailable - after connection timeout or when there's no network * - failed - when the connection strategy is not supported * * Options: * - unavailableTimeout - time to transition to unavailable state * - activityTimeout - time after which ping message should be sent * - pongTimeout - time for Pusher to respond with pong before reconnecting * * @param {String} key application key * @param {Object} options */ function ConnectionManager(key, options) { Pusher.EventsDispatcher.call(this); this.key = key; this.options = options || {}; this.state = "initialized"; this.connection = null; this.encrypted = !!options.encrypted; this.timeline = this.options.timeline; this.connectionCallbacks = this.buildConnectionCallbacks(); this.errorCallbacks = this.buildErrorCallbacks(); this.handshakeCallbacks = this.buildHandshakeCallbacks(this.errorCallbacks); var self = this; Pusher.Network.bind("online", function() { self.timeline.info({ netinfo: "online" }); if (self.state === "connecting" || self.state === "unavailable") { self.retryIn(0); } }); Pusher.Network.bind("offline", function() { self.timeline.info({ netinfo: "offline" }); if (self.state === "connected") { self.sendActivityCheck(); } }); this.updateStrategy(); } var prototype = ConnectionManager.prototype; Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); /** Establishes a connection to Pusher. * * Does nothing when connection is already established. See top-level doc * to find events emitted on connection attempts. */ prototype.connect = function() { if (this.connection || this.runner) { return; } if (!this.strategy.isSupported()) { this.updateState("failed"); return; } this.updateState("connecting"); this.startConnecting(); this.setUnavailableTimer(); }; /** Sends raw data. * * @param {String} data */ prototype.send = function(data) { if (this.connection) { return this.connection.send(data); } else { return false; } }; /** Sends an event. * * @param {String} name * @param {String} data * @param {String} [channel] * @returns {Boolean} whether message was sent or not */ prototype.send_event = function(name, data, channel) { if (this.connection) { return this.connection.send_event(name, data, channel); } else { return false; } }; /** Closes the connection. */ prototype.disconnect = function() { this.disconnectInternally(); this.updateState("disconnected"); }; prototype.isEncrypted = function() { return this.encrypted; }; /** @private */ prototype.startConnecting = function() { var self = this; var callback = function(error, handshake) { if (error) { self.runner = self.strategy.connect(0, callback); } else { if (handshake.action === "error") { self.emit("error", { type: "HandshakeError", error: handshake.error }); self.timeline.error({ handshakeError: handshake.error }); } else { self.abortConnecting(); // we don't support switching connections yet self.handshakeCallbacks[handshake.action](handshake); } } }; self.runner = self.strategy.connect(0, callback); }; /** @private */ prototype.abortConnecting = function() { if (this.runner) { this.runner.abort(); this.runner = null; } }; /** @private */ prototype.disconnectInternally = function() { this.abortConnecting(); this.clearRetryTimer(); this.clearUnavailableTimer(); this.stopActivityCheck(); if (this.connection) { var connection = this.abandonConnection(); connection.close(); } }; /** @private */ prototype.updateStrategy = function() { this.strategy = this.options.getStrategy({ key: this.key, timeline: this.timeline, encrypted: this.encrypted }); }; /** @private */ prototype.retryIn = function(delay) { var self = this; self.timeline.info({ action: "retry", delay: delay }); if (delay > 0) { self.emit("connecting_in", Math.round(delay / 1000)); } self.retryTimer = new Pusher.Timer(delay || 0, function() { self.disconnectInternally(); self.connect(); }); }; /** @private */ prototype.clearRetryTimer = function() { if (this.retryTimer) { this.retryTimer.ensureAborted(); this.retryTimer = null; } }; /** @private */ prototype.setUnavailableTimer = function() { var self = this; self.unavailableTimer = new Pusher.Timer( self.options.unavailableTimeout, function() { self.updateState("unavailable"); } ); }; /** @private */ prototype.clearUnavailableTimer = function() { if (this.unavailableTimer) { this.unavailableTimer.ensureAborted(); } }; /** @private */ prototype.sendActivityCheck = function() { var self = this; self.stopActivityCheck(); self.connection.ping(); // wait for pong response self.activityTimer = new Pusher.Timer( self.options.pongTimeout, function() { self.timeline.error({ pong_timed_out: self.options.pongTimeout }); self.retryIn(0); } ); }; /** @private */ prototype.resetActivityCheck = function() { var self = this; self.stopActivityCheck(); // send ping after inactivity if (!self.connection.handlesActivityChecks()) { self.activityTimer = new Pusher.Timer(self.activityTimeout, function() { self.sendActivityCheck(); }); } }; /** @private */ prototype.stopActivityCheck = function() { if (this.activityTimer) { this.activityTimer.ensureAborted(); } }; /** @private */ prototype.buildConnectionCallbacks = function() { var self = this; return { message: function(message) { // includes pong messages from server self.resetActivityCheck(); self.emit('message', message); }, ping: function() { self.send_event('pusher:pong', {}); }, activity: function() { self.resetActivityCheck(); }, error: function(error) { // just emit error to user - socket will already be closed by browser self.emit("error", { type: "WebSocketError", error: error }); }, closed: function() { self.abandonConnection(); if (self.shouldRetry()) { self.retryIn(1000); } } }; }; /** @private */ prototype.buildHandshakeCallbacks = function(errorCallbacks) { var self = this; return Pusher.Util.extend({}, errorCallbacks, { connected: function(handshake) { self.activityTimeout = Math.min( self.options.activityTimeout, handshake.activityTimeout, handshake.connection.activityTimeout || Infinity ); self.clearUnavailableTimer(); self.setConnection(handshake.connection); self.socket_id = self.connection.id; self.updateState("connected", { socket_id: self.socket_id }); } }); }; /** @private */ prototype.buildErrorCallbacks = function() { var self = this; function withErrorEmitted(callback) { return function(result) { if (result.error) { self.emit("error", { type: "WebSocketError", error: result.error }); } callback(result); }; } return { ssl_only: withErrorEmitted(function() { self.encrypted = true; self.updateStrategy(); self.retryIn(0); }), refused: withErrorEmitted(function() { self.disconnect(); }), backoff: withErrorEmitted(function() { self.retryIn(1000); }), retry: withErrorEmitted(function() { self.retryIn(0); }) }; }; /** @private */ prototype.setConnection = function(connection) { this.connection = connection; for (var event in this.connectionCallbacks) { this.connection.bind(event, this.connectionCallbacks[event]); } this.resetActivityCheck(); }; /** @private */ prototype.abandonConnection = function() { if (!this.connection) { return; } for (var event in this.connectionCallbacks) { this.connection.unbind(event, this.connectionCallbacks[event]); } var connection = this.connection; this.connection = null; return connection; }; /** @private */ prototype.updateState = function(newState, data) { var previousState = this.state; this.state = newState; if (previousState !== newState) { Pusher.debug('State changed', previousState + ' -> ' + newState); this.timeline.info({ state: newState, params: data }); this.emit('state_change', { previous: previousState, current: newState }); this.emit(newState, data); } }; /** @private */ prototype.shouldRetry = function() { return this.state === "connecting" || this.state === "connected"; }; Pusher.ConnectionManager = ConnectionManager; }).call(this); ;(function() { /** Really basic interface providing network availability info. * * Emits: * - online - when browser goes online * - offline - when browser goes offline */ function NetInfo() { Pusher.EventsDispatcher.call(this); var self = this; // This is okay, as IE doesn't support this stuff anyway. if (window.addEventListener !== undefined) { window.addEventListener("online", function() { self.emit('online'); }, false); window.addEventListener("offline", function() { self.emit('offline'); }, false); } } Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype); var prototype = NetInfo.prototype; /** Returns whether browser is online or not * * Offline means definitely offline (no connection to router). * Inverse does NOT mean definitely online (only currently supported in Safari * and even there only means the device has a connection to the router). * * @return {Boolean} */ prototype.isOnline = function() { if (window.navigator.onLine === undefined) { return true; } else { return window.navigator.onLine; } }; Pusher.NetInfo = NetInfo; Pusher.Network = new NetInfo(); }).call(this); ;(function() { /** Represents a collection of members of a presence channel. */ function Members() { this.reset(); } var prototype = Members.prototype; /** Returns member's info for given id. * * Resulting object containts two fields - id and info. * * @param {Number} id * @return {Object} member's info or null */ prototype.get = function(id) { if (Object.prototype.hasOwnProperty.call(this.members, id)) { return { id: id, info: this.members[id] }; } else { return null; } }; /** Calls back for each member in unspecified order. * * @param {Function} callback */ prototype.each = function(callback) { var self = this; Pusher.Util.objectApply(self.members, function(member, id) { callback(self.get(id)); }); }; /** Updates the id for connected member. For internal use only. */ prototype.setMyID = function(id) { this.myID = id; }; /** Handles subscription data. For internal use only. */ prototype.onSubscription = function(subscriptionData) { this.members = subscriptionData.presence.hash; this.count = subscriptionData.presence.count; this.me = this.get(this.myID); }; /** Adds a new member to the collection. For internal use only. */ prototype.addMember = function(memberData) { if (this.get(memberData.user_id) === null) { this.count++; } this.members[memberData.user_id] = memberData.user_info; return this.get(memberData.user_id); }; /** Adds a member from the collection. For internal use only. */ prototype.removeMember = function(memberData) { var member = this.get(memberData.user_id); if (member) { delete this.members[memberData.user_id]; this.count--; } return member; }; /** Resets the collection to the initial state. For internal use only. */ prototype.reset = function() { this.members = {}; this.count = 0; this.myID = null; this.me = null; }; Pusher.Members = Members; }).call(this); ;(function() { /** Provides base public channel interface with an event emitter. * * Emits: * - pusher:subscription_succeeded - after subscribing successfully * - other non-internal events * * @param {String} name * @param {Pusher} pusher */ function Channel(name, pusher) { Pusher.EventsDispatcher.call(this, function(event, data) { Pusher.debug('No callbacks on ' + name + ' for ' + event); }); this.name = name; this.pusher = pusher; this.subscribed = false; } var prototype = Channel.prototype; Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); /** Skips authorization, since public channels don't require it. * * @param {Function} callback */ prototype.authorize = function(socketId, callback) { return callback(false, {}); }; /** Triggers an event */ prototype.trigger = function(event, data) { if (event.indexOf("client-") !== 0) { throw new Pusher.Errors.BadEventName( "Event '" + event + "' does not start with 'client-'" ); } return this.pusher.send_event(event, data, this.name); }; /** Signals disconnection to the channel. For internal use only. */ prototype.disconnect = function() { this.subscribed = false; }; /** Handles an event. For internal use only. * * @param {String} event * @param {*} data */ prototype.handleEvent = function(event, data) { if (event.indexOf("pusher_internal:") === 0) { if (event === "pusher_internal:subscription_succeeded") { this.subscribed = true; this.emit("pusher:subscription_succeeded", data); } } else { this.emit(event, data); } }; /** Sends a subscription request. For internal use only. */ prototype.subscribe = function() { var self = this; self.authorize(self.pusher.connection.socket_id, function(error, data) { if (error) { self.handleEvent('pusher:subscription_error', data); } else { self.pusher.send_event('pusher:subscribe', { auth: data.auth, channel_data: data.channel_data, channel: self.name }); } }); }; /** Sends an unsubscription request. For internal use only. */ prototype.unsubscribe = function() { this.pusher.send_event('pusher:unsubscribe', { channel: this.name }); }; Pusher.Channel = Channel; }).call(this); ;(function() { /** Extends public channels to provide private channel interface. * * @param {String} name * @param {Pusher} pusher */ function PrivateChannel(name, pusher) { Pusher.Channel.call(this, name, pusher); } var prototype = PrivateChannel.prototype; Pusher.Util.extend(prototype, Pusher.Channel.prototype); /** Authorizes the connection to use the channel. * * @param {String} socketId * @param {Function} callback */ prototype.authorize = function(socketId, callback) { var authorizer = new Pusher.Channel.Authorizer(this, this.pusher.config); return authorizer.authorize(socketId, callback); }; Pusher.PrivateChannel = PrivateChannel; }).call(this); ;(function() { /** Adds presence channel functionality to private channels. * * @param {String} name * @param {Pusher} pusher */ function PresenceChannel(name, pusher) { Pusher.PrivateChannel.call(this, name, pusher); this.members = new Pusher.Members(); } var prototype = PresenceChannel.prototype; Pusher.Util.extend(prototype, Pusher.PrivateChannel.prototype); /** Authenticates the connection as a member of the channel. * * @param {String} socketId * @param {Function} callback */ prototype.authorize = function(socketId, callback) { var _super = Pusher.PrivateChannel.prototype.authorize; var self = this; _super.call(self, socketId, function(error, authData) { if (!error) { if (authData.channel_data === undefined) { Pusher.warn( "Invalid auth response for channel '" + self.name + "', expected 'channel_data' field" ); callback("Invalid auth response"); return; } var channelData = JSON.parse(authData.channel_data); self.members.setMyID(channelData.user_id); } callback(error, authData); }); }; /** Handles presence and subscription events. For internal use only. * * @param {String} event * @param {*} data */ prototype.handleEvent = function(event, data) { switch (event) { case "pusher_internal:subscription_succeeded": this.members.onSubscription(data); this.subscribed = true; this.emit("pusher:subscription_succeeded", this.members); break; case "pusher_internal:member_added": var addedMember = this.members.addMember(data); this.emit('pusher:member_added', addedMember); break; case "pusher_internal:member_removed": var removedMember = this.members.removeMember(data); if (removedMember) { this.emit('pusher:member_removed', removedMember); } break; default: Pusher.PrivateChannel.prototype.handleEvent.call(this, event, data); } }; /** Resets the channel state, including members map. For internal use only. */ prototype.disconnect = function() { this.members.reset(); Pusher.PrivateChannel.prototype.disconnect.call(this); }; Pusher.PresenceChannel = PresenceChannel; }).call(this); ;(function() { /** Handles a channel map. */ function Channels() { this.channels = {}; } var prototype = Channels.prototype; /** Creates or retrieves an existing channel by its name. * * @param {String} name * @param {Pusher} pusher * @return {Channel} */ prototype.add = function(name, pusher) { if (!this.channels[name]) { this.channels[name] = createChannel(name, pusher); } return this.channels[name]; }; /** Returns a list of all channels * * @return {Array} */ prototype.all = function(name) { return Pusher.Util.values(this.channels); }; /** Finds a channel by its name. * * @param {String} name * @return {Channel} channel or null if it doesn't exist */ prototype.find = function(name) { return this.channels[name]; }; /** Removes a channel from the map. * * @param {String} name */ prototype.remove = function(name) { var channel = this.channels[name]; delete this.channels[name]; return channel; }; /** Proxies disconnection signal to all channels. */ prototype.disconnect = function() { Pusher.Util.objectApply(this.channels, function(channel) { channel.disconnect(); }); }; function createChannel(name, pusher) { if (name.indexOf('private-') === 0) { return new Pusher.PrivateChannel(name, pusher); } else if (name.indexOf('presence-') === 0) { return new Pusher.PresenceChannel(name, pusher); } else { return new Pusher.Channel(name, pusher); } } Pusher.Channels = Channels; }).call(this); ;(function() { Pusher.Channel.Authorizer = function(channel, options) { this.channel = channel; this.type = options.authTransport; this.options = options; this.authOptions = (options || {}).auth || {}; }; Pusher.Channel.Authorizer.prototype = { composeQuery: function(socketId) { var query = '&socket_id=' + encodeURIComponent(socketId) + '&channel_name=' + encodeURIComponent(this.channel.name); for(var i in this.authOptions.params) { query += "&" + encodeURIComponent(i) + "=" + encodeURIComponent(this.authOptions.params[i]); } return query; }, authorize: function(socketId, callback) { return Pusher.authorizers[this.type].call(this, socketId, callback); } }; var nextAuthCallbackID = 1; Pusher.auth_callbacks = {}; Pusher.authorizers = { ajax: function(socketId, callback){ var self = this, xhr; if (Pusher.XHR) { xhr = new Pusher.XHR(); } else { xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP")); } xhr.open("POST", self.options.authEndpoint, true); // add request headers xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); for(var headerName in this.authOptions.headers) { xhr.setRequestHeader(headerName, this.authOptions.headers[headerName]); } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { var data, parsed = false; try { data = JSON.parse(xhr.responseText); parsed = true; } catch (e) { callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); } if (parsed) { // prevents double execution. callback(false, data); } } else { Pusher.warn("Couldn't get auth info from your webapp", xhr.status); callback(true, xhr.status); } } }; xhr.send(this.composeQuery(socketId)); return xhr; }, jsonp: function(socketId, callback){ if(this.authOptions.headers !== undefined) { Pusher.warn("Warn", "To send headers with the auth request, you must use AJAX, rather than JSONP."); } var callbackName = nextAuthCallbackID.toString(); nextAuthCallbackID++; var document = Pusher.Util.getDocument(); var script = document.createElement("script"); // Hacked wrapper. Pusher.auth_callbacks[callbackName] = function(data) { callback(false, data); }; var callback_name = "Pusher.auth_callbacks['" + callbackName + "']"; script.src = this.options.authEndpoint + '?callback=' + encodeURIComponent(callback_name) + this.composeQuery(socketId); var head = document.getElementsByTagName("head")[0] || document.documentElement; head.insertBefore( script, head.firstChild ); } }; }).call(this);