Created
January 15, 2015 01:04
-
-
Save leggetter/1c1658f0555a1a94180a to your computer and use it in GitHub Desktop.
pusher-js 2.2.3 but with a returnExportsGlobal UMD header which means it should work in browsers by Pusher being available as a global and with Browserify with CommonJS style require syntax. AMD has not been tested.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* Pusher JavaScript Library v2.2.3-umd | |
* http://pusher.com/ | |
* | |
* Copyright 2014, Pusher | |
* Released under the MIT licence. | |
*/ | |
// Uses Node, AMD or browser globals to create a module. This example creates | |
// a global even when AMD is used. This is useful if you have some scripts | |
// that are loaded by an AMD loader, but they still want access to globals. | |
// If you do not need to export a global for the AMD case, | |
// see returnExports.js. | |
// If you want something that will work in other stricter CommonJS environments, | |
// or if you need to create a circular dependency, see commonJsStrictGlobal.js | |
// Defines a module "Pusher". | |
(function (root, factory) { | |
if (typeof define === 'function' && define.amd) { | |
// AMD. Register as an anonymous module. | |
define(['Pusher'], function () { | |
return (root.Pusher = factory()); | |
}); | |
} else if (typeof exports === 'object') { | |
// Node. Does not work with strict CommonJS, but | |
// only CommonJS-like enviroments that support module.exports, | |
// like Node. | |
module.exports = factory(); | |
} else { | |
// Browser globals | |
root.Pusher = factory(); | |
} | |
}(this, function () { | |
;(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.3'; | |
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://js.pusher.com/'; | |
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, | |
httpPath: config.httpPath | |
}], | |
[":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.connection) { | |
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(); | |
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; | |
} | |
this.stopActivityCheck(); | |
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); | |
return Pusher; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment