From a060b08552716139611eae4eeddebdfc1b48088e Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Thu, 27 Feb 2014 20:53:22 +0100 Subject: [PATCH] Use not minified pusher, it's easier to debut and we minify it anyway --- assets/scripts/vendor/pusher.js | 4088 ++++++++++++++++++++++++++++++- 1 file changed, 3974 insertions(+), 114 deletions(-) diff --git a/assets/scripts/vendor/pusher.js b/assets/scripts/vendor/pusher.js index 10e3a8f2..d16cb9d9 100644 --- a/assets/scripts/vendor/pusher.js +++ b/assets/scripts/vendor/pusher.js @@ -6,117 +6,3977 @@ * Released under the MIT licence. */ -(function(){function b(a,d){(a===null||a===void 0)&&b.warn("Warning","You must pass your app key when you instantiate Pusher.");var d=d||{},c=this;this.key=a;this.config=b.Util.extend(b.getGlobalConfig(),d.cluster?b.getClusterConfig(d.cluster):{},d);this.channels=new b.Channels;this.global_emitter=new b.EventsDispatcher;this.sessionID=Math.floor(Math.random()*1E9);this.timeline=new b.Timeline(this.key,this.sessionID,{features:b.Util.getClientFeatures(),params:this.config.timelineParams||{},limit:50, -level:b.Timeline.INFO,version:b.VERSION});if(!this.config.disableStats)this.timelineSender=new b.TimelineSender(this.timeline,{host:this.config.statsHost,path:"/timeline"});this.connection=new b.ConnectionManager(this.key,b.Util.extend({getStrategy:function(a){return b.StrategyBuilder.build(b.getDefaultStrategy(c.config),b.Util.extend({},c.config,a))},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(){c.subscribeAll();c.timelineSender&&c.timelineSender.send(c.connection.isEncrypted())});this.connection.bind("message",function(a){var b=a.event.indexOf("pusher_internal:")===0;if(a.channel){var d=c.channel(a.channel);d&&d.handleEvent(a.event,a.data)}b||c.global_emitter.emit(a.event,a.data)});this.connection.bind("disconnected",function(){c.channels.disconnect()});this.connection.bind("error",function(a){b.warn("Error", -a)});b.instances.push(this);this.timeline.info({instances:b.instances.length});b.isReady&&c.connect()}var c=b.prototype;b.instances=[];b.isReady=!1;b.debug=function(){b.log&&b.log(b.Util.stringify.apply(this,arguments))};b.warn=function(){var a=b.Util.stringify.apply(this,arguments);window.console&&(window.console.warn?window.console.warn(a):window.console.log&&window.console.log(a));b.log&&b.log(a)};b.ready=function(){b.isReady=!0;for(var a=0,d=b.instances.length;a0)for(d=0;d0?this.loading[b].push(d):(this.loading[b]=[d],a(this.getPath(b),function(){c.loaded[b]=!0;if(c.loading[b]){for(var a=0;a>>6)+b(128|d&63):b(224|d>>>12&15)+b(128|d>>>6&63)+b(128|d&63)},d=function(a){var b=[0,2,1][a.length%3],a=a.charCodeAt(0)<<16|(a.length>1?a.charCodeAt(1):0)<<8|(a.length>2?a.charCodeAt(2):0);return["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(a>>>18),"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(a>>> -12&63),b>=2?"=":"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(a>>>6&63),b>=1?"=":"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(a&63)].join("")},f=window.btoa||function(a){return a.replace(/[\s\S]{1,3}/g,d)};Pusher.Base64={encode:function(b){return f(b.replace(/[^\x00-\x7F]/g,a))}}}).call(this); -(function(){function b(a){this.options=a}function c(a){return Pusher.Util.mapObject(a,function(a){typeof a==="object"&&(a=JSON.stringify(a));return encodeURIComponent(Pusher.Base64.encode(a.toString()))})}b.send=function(a,b){var c=new Pusher.JSONPRequest({url:a.url,receiver:a.receiverName,tagPrefix:a.tagPrefix}),g=a.receiver.register(function(a,d){c.cleanup();b(a,d)});return c.send(g,a.data,function(b){var c=a.receiver.unregister(g);c&&c(b)})};var a=b.prototype;a.send=function(a,b,e){if(this.script)return!1; -var g=this.options.tagPrefix||"_pusher_jsonp_",b=Pusher.Util.extend({},b,{receiver:this.options.receiver}),b=Pusher.Util.map(Pusher.Util.flatten(c(Pusher.Util.filterObject(b,function(a){return a!==void 0}))),Pusher.Util.method("join","=")).join("&");this.script=document.createElement("script");this.script.id=g+a;this.script.src=this.options.url+"/"+a+"?"+b;this.script.type="text/javascript";this.script.charset="UTF-8";this.script.onerror=this.script.onload=e;if(this.script.async===void 0&&document.attachEvent&& -/opera/i.test(navigator.userAgent))g=this.options.receiver||"Pusher.JSONP.receive",this.errorScript=document.createElement("script"),this.errorScript.text=g+"("+a+", true);",this.script.async=this.errorScript.async=!1;var h=this;this.script.onreadystatechange=function(){h.script&&/loaded|complete/.test(h.script.readyState)&&e(!0)};a=document.getElementsByTagName("head")[0];a.insertBefore(this.script,a.firstChild);this.errorScript&&a.insertBefore(this.errorScript,this.script.nextSibling);return!0}; -a.cleanup=function(){if(this.script&&this.script.parentNode)this.script.parentNode.removeChild(this.script),this.script=null;if(this.errorScript&&this.errorScript.parentNode)this.errorScript.parentNode.removeChild(this.errorScript),this.errorScript=null};Pusher.JSONPRequest=b}).call(this); -(function(){function b(){this.lastId=0;this.callbacks={}}var c=b.prototype;c.register=function(a){this.lastId++;var b=this.lastId;this.callbacks[b]=a;return b};c.unregister=function(a){if(this.callbacks[a]){var b=this.callbacks[a];delete this.callbacks[a];return b}else return null};c.receive=function(a,b,c){(a=this.unregister(a))&&a(b,c)};Pusher.JSONPReceiver=b;Pusher.JSONP=new b}).call(this); -(function(){function b(a,b,c){this.key=a;this.session=b;this.events=[];this.options=c||{};this.uniqueID=this.sent=0}var c=b.prototype;b.ERROR=3;b.INFO=6;b.DEBUG=7;c.log=function(a,c){if(this.options.level===void 0||a<=this.options.level)this.events.push(Pusher.Util.extend({},c,{timestamp:Pusher.Util.now(),level:a!==b.INFO?a:void 0})),this.options.limit&&this.events.length>this.options.limit&&this.events.shift()};c.error=function(a){this.log(b.ERROR,a)};c.info=function(a){this.log(b.INFO,a)};c.debug= -function(a){this.log(b.DEBUG,a)};c.isEmpty=function(){return this.events.length===0};c.send=function(a,b){var c=this,e=Pusher.Util.extend({session:c.session,bundle:c.sent+1,key:c.key,lib:"js",version:c.options.version,features:c.options.features,timeline:c.events},c.options.params);c.events=[];a(e,function(a,h){a||c.sent++;b&&b(a,h)});return!0};c.generateUniqueID=function(){this.uniqueID++;return this.uniqueID};Pusher.Timeline=b}).call(this); -(function(){function b(b,a){this.timeline=b;this.options=a||{}}b.prototype.send=function(b,a){if(!this.timeline.isEmpty()){var d=this,f="http"+(b?"s":"")+"://";d.timeline.send(function(a,b){var c={data:Pusher.Util.filterObject(a,function(a){return a!==void 0}),url:f+(d.host||d.options.host)+d.options.path,receiver:Pusher.JSONP};return Pusher.JSONPRequest.send(c,function(a,c){if(c&&c.host)d.host=c.host;b&&b(a,c)})},a)}};Pusher.TimelineSender=b}).call(this); -(function(){function b(a){this.strategies=a}function c(a,b,c){var f=Pusher.Util.map(a,function(a,d,f,e){return a.connect(b,c(d,e))});return{abort:function(){Pusher.Util.apply(f,d)},forceMinPriority:function(a){Pusher.Util.apply(f,function(b){b.forceMinPriority(a)})}}}function a(a){return Pusher.Util.all(a,function(a){return Boolean(a.error)})}function d(a){if(!a.error&&!a.aborted)a.abort(),a.aborted=!0}var f=b.prototype;f.isSupported=function(){return Pusher.Util.any(this.strategies,Pusher.Util.method("isSupported"))}; -f.connect=function(b,d){return c(this.strategies,b,function(b,c){return function(f,e){(c[b].error=f)?a(c)&&d(!0):(Pusher.Util.apply(c,function(a){a.forceMinPriority(e.transport.priority)}),d(null,e))}})};Pusher.BestConnectedEverStrategy=b}).call(this); -(function(){function b(a,b,c){this.strategy=a;this.transports=b;this.ttl=c.ttl||18E5;this.encrypted=c.encrypted;this.timeline=c.timeline}function c(a){return"pusherTransport"+(a?"Encrypted":"Unencrypted")}function a(a){var b=Pusher.Util.getLocalStorage();if(b)try{var f=b[c(a)];if(f)return JSON.parse(f)}catch(i){d(a)}return null}function d(a){var b=Pusher.Util.getLocalStorage();if(b)try{delete b[c(a)]}catch(d){}}var f=b.prototype;f.isSupported=function(){return this.strategy.isSupported()};f.connect= -function(b,f){var h=this.encrypted,i=a(h),j=[this.strategy];if(i&&i.timestamp+this.ttl>=Pusher.Util.now()){var k=this.transports[i.transport];k&&(this.timeline.info({cached:!0,transport:i.transport,latency:i.latency}),j.push(new Pusher.SequentialStrategy([k],{timeout:i.latency*2,failFast:!0})))}var m=Pusher.Util.now(),l=j.pop().connect(b,function p(a,i){if(a)d(h),j.length>0?(m=Pusher.Util.now(),l=j.pop().connect(b,p)):f(a);else{var k=i.transport.name,q=Pusher.Util.now()-m,o=Pusher.Util.getLocalStorage(); -if(o)try{o[c(h)]=JSON.stringify({timestamp:Pusher.Util.now(),transport:k,latency:q})}catch(r){}f(null,i)}});return{abort:function(){l.abort()},forceMinPriority:function(a){b=a;l&&l.forceMinPriority(a)}}};Pusher.CachedStrategy=b}).call(this); -(function(){function b(a,b){this.strategy=a;this.options={delay:b.delay}}var c=b.prototype;c.isSupported=function(){return this.strategy.isSupported()};c.connect=function(a,b){var c=this.strategy,e,g=new Pusher.Timer(this.options.delay,function(){e=c.connect(a,b)});return{abort:function(){g.ensureAborted();e&&e.abort()},forceMinPriority:function(b){a=b;e&&e.forceMinPriority(b)}}};Pusher.DelayedStrategy=b}).call(this); -(function(){function b(a){this.strategy=a}var c=b.prototype;c.isSupported=function(){return this.strategy.isSupported()};c.connect=function(a,b){var c=this.strategy.connect(a,function(a,g){g&&c.abort();b(a,g)});return c};Pusher.FirstConnectedStrategy=b}).call(this); -(function(){function b(a,b,c){this.test=a;this.trueBranch=b;this.falseBranch=c}var c=b.prototype;c.isSupported=function(){return(this.test()?this.trueBranch:this.falseBranch).isSupported()};c.connect=function(a,b){return(this.test()?this.trueBranch:this.falseBranch).connect(a,b)};Pusher.IfStrategy=b}).call(this); -(function(){function b(a,b){this.strategies=a;this.loop=Boolean(b.loop);this.failFast=Boolean(b.failFast);this.timeout=b.timeout;this.timeoutLimit=b.timeoutLimit}var c=b.prototype;c.isSupported=function(){return Pusher.Util.any(this.strategies,Pusher.Util.method("isSupported"))};c.connect=function(a,b){var c=this,e=this.strategies,g=0,h=this.timeout,i=null,j=function(k,m){m?b(null,m):(g+=1,c.loop&&(g%=e.length),g0&&(g=new Pusher.Timer(c.timeout,function(){h.abort();e(!0)}));h=a.connect(b,function(a,b){if(!a||!g||!g.isRunning()||c.failFast)g&&g.ensureAborted(),e(a,b)});return{abort:function(){g&&g.ensureAborted();h.abort()},forceMinPriority:function(a){h.forceMinPriority(a)}}}; -Pusher.SequentialStrategy=b}).call(this); -(function(){function b(a,b,c,g){this.name=a;this.priority=b;this.transport=c;this.options=g||{}}function c(a,b){Pusher.Util.defer(function(){b(a)});return{abort:function(){},forceMinPriority:function(){}}}var a=b.prototype;a.isSupported=function(){return this.transport.isSupported({encrypted:this.options.encrypted})};a.connect=function(a,b){if(this.isSupported()){if(this.priority0};c.reportDeath=function(){this.livesLeft-=1};Pusher.TransportManager=b}).call(this); -(function(){function b(a){return function(b){return[a.apply(this,arguments),b]}}function c(a,b){if(a.length===0)return[[],b];var f=d(a[0],b),e=c(a.slice(1),f[1]);return[[f[0]].concat(e[0]),e[1]]}function a(a,b){if(typeof a[0]==="string"&&a[0].charAt(0)===":"){var f=b[a[0].slice(1)];if(a.length>1){if(typeof f!=="function")throw"Calling non-function "+a[0];var e=[Pusher.Util.extend({},b)].concat(Pusher.Util.map(a.slice(1),function(a){return d(a,Pusher.Util.extend({},b))[0]}));return f.apply(this,e)}else return[f, -b]}else return c(a,b)}function d(b,c){if(typeof b==="string"){var d;if(typeof b==="string"&&b.charAt(0)===":"){d=c[b.slice(1)];if(d===void 0)throw"Undefined symbol "+b;d=[d,c]}else d=[b,c];return d}else if(typeof b==="object"&&b instanceof Array&&b.length>0)return a(b,c);return[b,c]}var f={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}, -e={isSupported:function(){return!1},connect:function(a,b){var c=Pusher.Util.defer(function(){b(new Pusher.Errors.UnsupportedStrategy)});return{abort:function(){c.ensureAborted()},forceMinPriority:function(){}}}},g={def:function(a,b,c){if(a[b]!==void 0)throw"Redefining symbol "+b;a[b]=c;return[void 0,a]},def_transport:function(a,b,c,d,g,l){var n=f[c];if(!n)throw new Pusher.Errors.UnsupportedTransport(c);c=(!a.enabledTransports||Pusher.Util.arrayIndexOf(a.enabledTransports,b)!==-1)&&(!a.disabledTransports|| -Pusher.Util.arrayIndexOf(a.disabledTransports,b)===-1)&&(b!=="flash"||a.disableFlash!==!0)?new Pusher.TransportStrategy(b,d,l?l.getAssistant(n):n,Pusher.Util.extend({key:a.key,encrypted:a.encrypted,timeline:a.timeline,ignoreNullOrigin:a.ignoreNullOrigin},g)):e;d=a.def(a,b,c)[1];d.transports=a.transports||{};d.transports[b]=c;return[void 0,d]},transport_manager:b(function(a,b){return new Pusher.TransportManager(b)}),sequential:b(function(a,b){var c=Array.prototype.slice.call(arguments,2);return new Pusher.SequentialStrategy(c, -b)}),cached:b(function(a,b,c){return new Pusher.CachedStrategy(c,a.transports,{ttl:b,timeline:a.timeline,encrypted:a.encrypted})}),first_connected:b(function(a,b){return new Pusher.FirstConnectedStrategy(b)}),best_connected_ever:b(function(){var a=Array.prototype.slice.call(arguments,1);return new Pusher.BestConnectedEverStrategy(a)}),delayed:b(function(a,b,c){return new Pusher.DelayedStrategy(c,{delay:b})}),"if":b(function(a,b,c,d){return new Pusher.IfStrategy(b,c,d)}),is_supported:b(function(a, -b){return function(){return b.isSupported()}})};Pusher.StrategyBuilder={build:function(a,b){var c=Pusher.Util.extend({},g,b);return d(a,c)[1].strategy}}}).call(this); -(function(){Pusher.Protocol={decodeMessage:function(b){try{var c=JSON.parse(b.data);if(typeof c.data==="string")try{c.data=JSON.parse(c.data)}catch(a){if(!(a instanceof SyntaxError))throw a;}return c}catch(d){throw{type:"MessageParseError",error:d,data:b.data};}},encodeMessage:function(b){return JSON.stringify(b)},processHandshake:function(b){b=this.decodeMessage(b);if(b.event==="pusher:connection_established"){if(!b.data.activity_timeout)throw"No activity timeout specified in handshake";return{action:"connected", -id:b.data.socket_id,activityTimeout:b.data.activity_timeout*1E3}}else if(b.event==="pusher:error")return{action:this.getCloseAction(b.data),error:this.getCloseError(b.data)};else throw"Invalid handshake";},getCloseAction:function(b){return b.code<4E3?b.code>=1002&&b.code<=1004?"backoff":null:b.code===4E3?"ssl_only":b.code<4100?"refused":b.code<4200?"backoff":b.code<4300?"retry":"refused"},getCloseError:function(b){return b.code!==1E3&&b.code!==1001?{type:"PusherError",data:{code:b.code,message:b.reason|| -b.message}}:null}}}).call(this); -(function(){function b(a,b){Pusher.EventsDispatcher.call(this);this.id=a;this.transport=b;this.activityTimeout=b.activityTimeout;this.bindListeners()}var c=b.prototype;Pusher.Util.extend(c,Pusher.EventsDispatcher.prototype);c.supportsPing=function(){return this.transport.supportsPing()};c.send=function(a){return this.transport.send(a)};c.send_event=function(a,b,c){a={event:a,data:b};if(c)a.channel=c;Pusher.debug("Event sent",a);return this.send(Pusher.Protocol.encodeMessage(a))};c.close=function(){this.transport.close()}; -c.bindListeners=function(){var a=this,b=function(b){var c;try{c=Pusher.Protocol.decodeMessage(b)}catch(d){a.emit("error",{type:"MessageParseError",error:d,data:b.data})}if(c!==void 0){Pusher.debug("Event recd",c);switch(c.event){case "pusher:error":a.emit("error",{type:"PusherError",data:c.data});break;case "pusher:ping":a.emit("ping");break;case "pusher:pong":a.emit("pong")}a.emit("message",c)}},c=function(b){a.emit("error",{type:"WebSocketError",error:b})},e=function(g){a.transport.unbind("closed", -e);a.transport.unbind("error",c);a.transport.unbind("message",b);g&&g.code&&a.handleCloseEvent(g);a.transport=null;a.emit("closed")};a.transport.bind("message",b);a.transport.bind("error",c);a.transport.bind("closed",e)};c.handleCloseEvent=function(a){var b=Pusher.Protocol.getCloseAction(a);(a=Pusher.Protocol.getCloseError(a))&&this.emit("error",a);b&&this.emit(b)};Pusher.Connection=b}).call(this); -(function(){function b(a,b){this.transport=a;this.callback=b;this.bindListeners()}var c=b.prototype;c.close=function(){this.unbindListeners();this.transport.close()};c.bindListeners=function(){var a=this;a.onMessage=function(b){a.unbindListeners();try{var c=Pusher.Protocol.processHandshake(b);c.action==="connected"?a.finish("connected",{connection:new Pusher.Connection(c.id,a.transport),activityTimeout:c.activityTimeout}):(a.finish(c.action,{error:c.error}),a.transport.close())}catch(e){a.finish("error", -{error:e}),a.transport.close()}};a.onClosed=function(b){a.unbindListeners();var c=Pusher.Protocol.getCloseAction(b)||"backoff",b=Pusher.Protocol.getCloseError(b);a.finish(c,{error:b})};a.transport.bind("message",a.onMessage);a.transport.bind("closed",a.onClosed)};c.unbindListeners=function(){this.transport.unbind("message",this.onMessage);this.transport.unbind("closed",this.onClosed)};c.finish=function(a,b){this.callback(Pusher.Util.extend({transport:this.transport,action:a},b))};Pusher.Handshake= -b}).call(this); -(function(){function b(a,b){Pusher.EventsDispatcher.call(this);this.key=a;this.options=b||{};this.state="initialized";this.connection=null;this.encrypted=!!b.encrypted;this.timeline=this.options.timeline;this.connectionCallbacks=this.buildConnectionCallbacks();this.errorCallbacks=this.buildErrorCallbacks();this.handshakeCallbacks=this.buildHandshakeCallbacks(this.errorCallbacks);var c=this;Pusher.Network.bind("online",function(){c.timeline.info({netinfo:"online"});(c.state==="connecting"||c.state=== -"unavailable")&&c.retryIn(0)});Pusher.Network.bind("offline",function(){c.timeline.info({netinfo:"offline"});c.state==="connected"&&c.sendActivityCheck()});this.updateStrategy()}var c=b.prototype;Pusher.Util.extend(c,Pusher.EventsDispatcher.prototype);c.connect=function(){!this.connection&&!this.runner&&(this.strategy.isSupported()?(this.updateState("connecting"),this.startConnecting(),this.setUnavailableTimer()):this.updateState("failed"))};c.send=function(a){return this.connection?this.connection.send(a): -!1};c.send_event=function(a,b,c){return this.connection?this.connection.send_event(a,b,c):!1};c.disconnect=function(){this.disconnectInternally();this.updateState("disconnected")};c.isEncrypted=function(){return this.encrypted};c.startConnecting=function(){var a=this,b=function(c,e){c?a.runner=a.strategy.connect(0,b):e.action==="error"?(a.emit("error",{type:"HandshakeError",error:e.error}),a.timeline.error({handshakeError:e.error})):(a.abortConnecting(),a.handshakeCallbacks[e.action](e))};a.runner= -a.strategy.connect(0,b)};c.abortConnecting=function(){if(this.runner)this.runner.abort(),this.runner=null};c.disconnectInternally=function(){this.abortConnecting();this.clearRetryTimer();this.clearUnavailableTimer();this.stopActivityCheck();this.connection&&this.abandonConnection().close()};c.updateStrategy=function(){this.strategy=this.options.getStrategy({key:this.key,timeline:this.timeline,encrypted:this.encrypted})};c.retryIn=function(a){var b=this;b.timeline.info({action:"retry",delay:a});a> -0&&b.emit("connecting_in",Math.round(a/1E3));b.retryTimer=new Pusher.Timer(a||0,function(){b.disconnectInternally();b.connect()})};c.clearRetryTimer=function(){if(this.retryTimer)this.retryTimer.ensureAborted(),this.retryTimer=null};c.setUnavailableTimer=function(){var a=this;a.unavailableTimer=new Pusher.Timer(a.options.unavailableTimeout,function(){a.updateState("unavailable")})};c.clearUnavailableTimer=function(){this.unavailableTimer&&this.unavailableTimer.ensureAborted()};c.sendActivityCheck= -function(){var a=this;a.stopActivityCheck();a.send_event("pusher:ping",{});a.activityTimer=new Pusher.Timer(a.options.pongTimeout,function(){a.timeline.error({pong_timed_out:a.options.pongTimeout});a.retryIn(0)})};c.resetActivityCheck=function(){var a=this;a.stopActivityCheck();if(!a.connection.supportsPing())a.activityTimer=new Pusher.Timer(a.activityTimeout,function(){a.sendActivityCheck()})};c.stopActivityCheck=function(){this.activityTimer&&this.activityTimer.ensureAborted()};c.buildConnectionCallbacks= -function(){var a=this;return{message:function(b){a.resetActivityCheck();a.emit("message",b)},ping:function(){a.send_event("pusher:pong",{})},error:function(b){a.emit("error",{type:"WebSocketError",error:b})},closed:function(){a.abandonConnection();a.shouldRetry()&&a.retryIn(1E3)}}};c.buildHandshakeCallbacks=function(a){var b=this;return Pusher.Util.extend({},a,{connected:function(a){b.activityTimeout=Math.min(b.options.activityTimeout,a.activityTimeout,a.connection.activityTimeout||Infinity);b.clearUnavailableTimer(); -b.setConnection(a.connection);b.socket_id=b.connection.id;b.updateState("connected",{socket_id:b.socket_id})}})};c.buildErrorCallbacks=function(){function a(a){return function(c){c.error&&b.emit("error",{type:"WebSocketError",error:c.error});a(c)}}var b=this;return{ssl_only:a(function(){b.encrypted=!0;b.updateStrategy();b.retryIn(0)}),refused:a(function(){b.disconnect()}),backoff:a(function(){b.retryIn(1E3)}),retry:a(function(){b.retryIn(0)})}};c.setConnection=function(a){this.connection=a;for(var b in this.connectionCallbacks)this.connection.bind(b, -this.connectionCallbacks[b]);this.resetActivityCheck()};c.abandonConnection=function(){if(this.connection){for(var a in this.connectionCallbacks)this.connection.unbind(a,this.connectionCallbacks[a]);a=this.connection;this.connection=null;return a}};c.updateState=function(a,b){var c=this.state;this.state=a;c!==a&&(Pusher.debug("State changed",c+" -> "+a),this.timeline.info({state:a,params:b}),this.emit("state_change",{previous:c,current:a}),this.emit(a,b))};c.shouldRetry=function(){return this.state=== -"connecting"||this.state==="connected"};Pusher.ConnectionManager=b}).call(this); -(function(){function b(){Pusher.EventsDispatcher.call(this);var b=this;window.addEventListener!==void 0&&(window.addEventListener("online",function(){b.emit("online")},!1),window.addEventListener("offline",function(){b.emit("offline")},!1))}Pusher.Util.extend(b.prototype,Pusher.EventsDispatcher.prototype);b.prototype.isOnline=function(){return window.navigator.onLine===void 0?!0:window.navigator.onLine};Pusher.NetInfo=b;Pusher.Network=new b}).call(this); -(function(){function b(){this.reset()}var c=b.prototype;c.get=function(a){return Object.prototype.hasOwnProperty.call(this.members,a)?{id:a,info:this.members[a]}:null};c.each=function(a){var b=this;Pusher.Util.objectApply(b.members,function(c,e){a(b.get(e))})};c.setMyID=function(a){this.myID=a};c.onSubscription=function(a){this.members=a.presence.hash;this.count=a.presence.count;this.me=this.get(this.myID)};c.addMember=function(a){this.get(a.user_id)===null&&this.count++;this.members[a.user_id]=a.user_info; -return this.get(a.user_id)};c.removeMember=function(a){var b=this.get(a.user_id);b&&(delete this.members[a.user_id],this.count--);return b};c.reset=function(){this.members={};this.count=0;this.me=this.myID=null};Pusher.Members=b}).call(this); -(function(){function b(a,b){Pusher.EventsDispatcher.call(this,function(b){Pusher.debug("No callbacks on "+a+" for "+b)});this.name=a;this.pusher=b;this.subscribed=!1}var c=b.prototype;Pusher.Util.extend(c,Pusher.EventsDispatcher.prototype);c.authorize=function(a,b){return b(!1,{})};c.trigger=function(a,b){if(a.indexOf("client-")!==0)throw new Pusher.Errors.BadEventName("Event '"+a+"' does not start with 'client-'");return this.pusher.send_event(a,b,this.name)};c.disconnect=function(){this.subscribed= -!1};c.handleEvent=function(a,b){if(a.indexOf("pusher_internal:")===0){if(a==="pusher_internal:subscription_succeeded")this.subscribed=!0,this.emit("pusher:subscription_succeeded",b)}else this.emit(a,b)};c.subscribe=function(){var a=this;a.authorize(a.pusher.connection.socket_id,function(b,c){b?a.handleEvent("pusher:subscription_error",c):a.pusher.send_event("pusher:subscribe",{auth:c.auth,channel_data:c.channel_data,channel:a.name})})};c.unsubscribe=function(){this.pusher.send_event("pusher:unsubscribe", -{channel:this.name})};Pusher.Channel=b}).call(this);(function(){function b(a,b){Pusher.Channel.call(this,a,b)}var c=b.prototype;Pusher.Util.extend(c,Pusher.Channel.prototype);c.authorize=function(a,b){return(new Pusher.Channel.Authorizer(this,this.pusher.config)).authorize(a,b)};Pusher.PrivateChannel=b}).call(this); -(function(){function b(a,b){Pusher.PrivateChannel.call(this,a,b);this.members=new Pusher.Members}var c=b.prototype;Pusher.Util.extend(c,Pusher.PrivateChannel.prototype);c.authorize=function(a,b){var c=this;Pusher.PrivateChannel.prototype.authorize.call(c,a,function(a,g){if(!a){if(g.channel_data===void 0){Pusher.warn("Invalid auth response for channel '"+c.name+"', expected 'channel_data' field");b("Invalid auth response");return}var h=JSON.parse(g.channel_data);c.members.setMyID(h.user_id)}b(a,g)})}; -c.handleEvent=function(a,b){switch(a){case "pusher_internal:subscription_succeeded":this.members.onSubscription(b);this.subscribed=!0;this.emit("pusher:subscription_succeeded",this.members);break;case "pusher_internal:member_added":this.emit("pusher:member_added",this.members.addMember(b));break;case "pusher_internal:member_removed":var c=this.members.removeMember(b);c&&this.emit("pusher:member_removed",c);break;default:Pusher.PrivateChannel.prototype.handleEvent.call(this,a,b)}};c.disconnect=function(){this.members.reset(); -Pusher.PrivateChannel.prototype.disconnect.call(this)};Pusher.PresenceChannel=b}).call(this); -(function(){function b(){this.channels={}}var c=b.prototype;c.add=function(a,b){this.channels[a]||(this.channels[a]=a.indexOf("private-")===0?new Pusher.PrivateChannel(a,b):a.indexOf("presence-")===0?new Pusher.PresenceChannel(a,b):new Pusher.Channel(a,b));return this.channels[a]};c.all=function(){return Pusher.Util.values(this.channels)};c.find=function(a){return this.channels[a]};c.remove=function(a){var b=this.channels[a];delete this.channels[a];return b};c.disconnect=function(){Pusher.Util.objectApply(this.channels, -function(a){a.disconnect()})};Pusher.Channels=b}).call(this); -(function(){Pusher.Channel.Authorizer=function(b,a){this.channel=b;this.type=a.authTransport;this.options=a;this.authOptions=(a||{}).auth||{}};Pusher.Channel.Authorizer.prototype={composeQuery:function(b){var b="&socket_id="+encodeURIComponent(b)+"&channel_name="+encodeURIComponent(this.channel.name),a;for(a in this.authOptions.params)b+="&"+encodeURIComponent(a)+"="+encodeURIComponent(this.authOptions.params[a]);return b},authorize:function(b,a){return Pusher.authorizers[this.type].call(this,b,a)}}; -var b=1;Pusher.auth_callbacks={};Pusher.authorizers={ajax:function(b,a){var d;d=Pusher.XHR?new Pusher.XHR:window.XMLHttpRequest?new window.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");d.open("POST",this.options.authEndpoint,!0);d.setRequestHeader("Content-Type","application/x-www-form-urlencoded");for(var f in this.authOptions.headers)d.setRequestHeader(f,this.authOptions.headers[f]);d.onreadystatechange=function(){if(d.readyState==4)if(d.status==200){var b,c=!1;try{b=JSON.parse(d.responseText), -c=!0}catch(f){a(!0,"JSON returned from webapp was invalid, yet status code was 200. Data was: "+d.responseText)}c&&a(!1,b)}else Pusher.warn("Couldn't get auth info from your webapp",d.status),a(!0,d.status)};d.send(this.composeQuery(b));return d},jsonp:function(c,a){this.authOptions.headers!==void 0&&Pusher.warn("Warn","To send headers with the auth request, you must use AJAX, rather than JSONP.");var d=b.toString();b++;var f=Pusher.Util.getDocument(),e=f.createElement("script");Pusher.auth_callbacks[d]= -function(b){a(!1,b)};e.src=this.options.authEndpoint+"?callback="+encodeURIComponent("Pusher.auth_callbacks['"+d+"']")+this.composeQuery(c);d=f.getElementsByTagName("head")[0]||f.documentElement;d.insertBefore(e,d.firstChild)}}}).call(this); +;(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, { + 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" + }); + } + + var getStrategy = function(options) { + return Pusher.StrategyBuilder.build( + Pusher.getDefaultStrategy(self.config), + Pusher.Util.extend({}, self.config, options) + ); + }; + + 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.getDocumentLocation().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() { + /** Cross-browser compatible timer abstraction. + * + * @param {Number} delay + * @param {Function} callback + */ + function Timer(delay, callback) { + var self = this; + + this.timeout = setTimeout(function() { + if (self.timeout !== null) { + callback(); + self.timeout = null; + } + }, delay); + } + var prototype = Timer.prototype; + + /** Returns whether the timer is still running. + * + * @return {Boolean} + */ + prototype.isRunning = function() { + return this.timeout !== null; + }; + + /** Aborts a timer when it's running. */ + prototype.ensureAborted = function() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + }; + + Pusher.Timer = Timer; +}).call(this); + +;(function() { + /** Cross-browser compatible periodic timer abstraction. + * + * @param {Number} interval + * @param {Function} callback + */ + function PeriodicTimer(interval, callback) { + var self = this; + + this.interval = setInterval(function() { + if (self.interval !== null) { + callback(); + } + }, interval); + } + var prototype = PeriodicTimer.prototype; + + /** Returns whether the timer is still running. + * + * @return {Boolean} + */ + prototype.isRunning = function() { + return this.interval !== null; + }; + + /** Aborts a timer when it's running. */ + prototype.ensureAborted = function() { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + }; + + Pusher.PeriodicTimer = PeriodicTimer; +}).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) { + for (var i = 0; i < array.length; i++) { + f(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; + }, + + getDocumentLocation: function() { + return Pusher.Util.getDocument().location; + }, + + 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 XHR && (new XHR()).withCredentials !== undefined; + }, + + isXDRSupported: function(encrypted) { + var originProtocol = Pusher.Util.getDocumentLocation().protocol; + var requestedProtocol = encrypted ? "https:" : "http:"; + return window.XDomainRequest && originProtocol === requestedProtocol; + } + }; +}).call(this); + +;(function() { + Pusher.VERSION = '2.2.0-rc1'; + Pusher.PROTOCOL = 7; + + // DEPRECATED: WS connection parameters + Pusher.host = 'ws.pusherapp.com'; + Pusher.ws_port = 80; + Pusher.wss_port = 443; + // DEPRECATED: SockJS fallback parameters + Pusher.sockjs_host = 'sockjs.pusher.com'; + Pusher.sockjs_http_port = 80; + Pusher.sockjs_https_port = 443; + Pusher.sockjs_path = "/pusher"; + // DEPRECATED: Stats + Pusher.stats_host = 'stats.pusher.com'; + // DEPRECATED: Other settings + Pusher.channel_auth_endpoint = '/pusher/auth'; + Pusher.channel_auth_transport = 'ajax'; + Pusher.activity_timeout = 120000; + Pusher.pong_timeout = 30000; + Pusher.unavailable_timeout = 10000; + // CDN configuration + Pusher.cdn_http = 'http://js.pusher.com/'; + Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'; + Pusher.dependency_suffix = ''; + + Pusher.getDefaultStrategy = function(config) { + return [ + [":def", "ws_options", { + hostUnencrypted: config.wsHost + ":" + config.wsPort, + hostEncrypted: config.wssHost + ":" + config.wssPort + }], + [":def", "sockjs_options", { + hostUnencrypted: config.httpHost + ":" + config.httpPort, + hostEncrypted: config.httpsHost + ":" + config.httpsPort + }], + [":def", "timeouts", { + loop: true, + timeout: 15000, + timeoutLimit: 60000 + }], + + [":def", "ws_manager", [":transport_manager", { + lives: 2, + minPingDelay: 10000, + maxPingDelay: config.activity_timeout + }]], + [":def", "streaming_manager", [":transport_manager", { + lives: 2, + minPingDelay: 10000, + maxPingDelay: config.activity_timeout + }]], + + [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"], + [":def_transport", "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", "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"], [ + ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":http_fallback_loop"]] + ], [":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, + wssHost: "wss.pusherapp.com", + wssPort: Pusher.wss_port, + httpHost: Pusher.sockjs_host, + httpPort: Pusher.sockjs_http_port, + httpsHost: "sockjss.pusher.com", + 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", + wssHost: "wss-" + clusterName + ".pusher.com", + httpHost: "sockjs-" + clusterName + ".pusher.com", + httpsHost: "sockjss-" + 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) { + this.callbacks.add(eventName, callback); + return this; + }; + + prototype.bind_all = function(callback) { + this.global_callbacks.push(callback); + return this; + }; + + prototype.unbind = function(eventName, callback) { + this.callbacks.remove(eventName, callback); + 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](data); + } + } else if (this.failThrough) { + this.failThrough(eventName, data); + } + + return this; + }; + + /** Callback registry helper. */ + + function CallbackRegistry() { + this._callbacks = {}; + } + + CallbackRegistry.prototype.get = function(eventName) { + return this._callbacks[this._prefix(eventName)]; + }; + + CallbackRegistry.prototype.add = function(eventName, callback) { + var prefixedEventName = this._prefix(eventName); + this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || []; + this._callbacks[prefixedEventName].push(callback); + }; + + CallbackRegistry.prototype.remove = function(eventName, callback) { + var callbacks = this.get(eventName); + if (callbacks) { + var index = arrayIndexOf(callbacks, callback); + if (index !== -1) { + var callbacksCopy = callbacks.slice(0); + callbacksCopy.splice(index, 1); + this._callbacks[this._prefix(eventName)] = callbacksCopy; + } + } + }; + + CallbackRegistry.prototype._prefix = function(eventName) { + return "_" + eventName; + }; + + function arrayIndexOf(array, item) { + 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; + } + + Pusher.EventsDispatcher = EventsDispatcher; +}).call(this); + +;(function() { + /** Handles loading dependency files. + * + * 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.loading = {}; + this.loaded = {}; + } + var prototype = DependencyLoader.prototype; + + /** Loads the dependency from CDN. + * + * @param {String} name + * @param {Function} callback + */ + prototype.load = function(name, callback) { + var self = this; + + if (this.loaded[name]) { + callback(); + } else if (this.loading[name] && this.loading[name].length > 0) { + this.loading[name].push(callback); + } else { + this.loading[name] = [callback]; + + require(this.getPath(name), function() { + self.loaded[name] = true; + + if (self.loading[name]) { + for (var i = 0; i < self.loading[name].length; i++) { + self.loading[name][i](); + } + delete self.loading[name]; + } + }); + } + }; + + /** Returns a root URL for pusher-js CDN. + * + * @returns {String} + */ + prototype.getRoot = function(options) { + var cdn; + var protocol = Pusher.Util.getDocumentLocation().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'; + }; + + function handleScriptLoaded(elem, callback) { + if (Pusher.Util.getDocument().addEventListener) { + elem.addEventListener('load', callback, false); + } else { + elem.attachEvent('onreadystatechange', function () { + if (elem.readyState === 'loaded' || elem.readyState === 'complete') { + callback(); + } + }); + } + } + + function require(src, callback) { + var document = Pusher.Util.getDocument(); + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + + script.setAttribute('src', src); + script.setAttribute("type","text/javascript"); + script.setAttribute('async', true); + + handleScriptLoaded(script, function() { + // workaround for an Opera issue + setTimeout(callback, 0); + }); + + head.appendChild(script); + } + + Pusher.DependencyLoader = DependencyLoader; +}).call(this); + +;(function() { + Pusher.Dependencies = new Pusher.DependencyLoader({ + cdn_http: Pusher.cdn_http, + cdn_https: Pusher.cdn_https, + version: Pusher.VERSION, + suffix: Pusher.dependency_suffix + }); + + // Support Firefox versions which prefix WebSocket + if (!window.WebSocket && window.MozWebSocket) { + window.WebSocket = window.MozWebSocket; + } + + 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() { + + function JSONPRequest(options) { + this.options = options; + } + + JSONPRequest.send = function(options, callback) { + var request = new Pusher.JSONPRequest({ + url: options.url, + receiver: options.receiverName, + tagPrefix: options.tagPrefix + }); + var id = options.receiver.register(function(error, result) { + request.cleanup(); + callback(error, result); + }); + + return request.send(id, options.data, function(error) { + var callback = options.receiver.unregister(id); + if (callback) { + callback(error); + } + }); + }; + + var prototype = JSONPRequest.prototype; + + prototype.send = function(id, data, callback) { + if (this.script) { + return false; + } + + var tagPrefix = this.options.tagPrefix || "_pusher_jsonp_"; + + var params = Pusher.Util.extend( + {}, data, { receiver: this.options.receiver } + ); + var query = Pusher.Util.map( + Pusher.Util.flatten( + encodeData( + Pusher.Util.filterObject(params, function(value) { + return value !== undefined; + }) + ) + ), + Pusher.Util.method("join", "=") + ).join("&"); + + this.script = document.createElement("script"); + this.script.id = tagPrefix + id; + this.script.src = this.options.url + "/" + id + "?" + query; + this.script.type = "text/javascript"; + this.script.charset = "UTF-8"; + this.script.onerror = this.script.onload = callback; + + // Opera<11.6 hack for missing onerror callback + if (this.script.async === undefined && document.attachEvent) { + if (/opera/i.test(navigator.userAgent)) { + var receiverName = this.options.receiver || "Pusher.JSONP.receive"; + this.errorScript = document.createElement("script"); + this.errorScript.text = receiverName + "(" + id + ", true);"; + this.script.async = this.errorScript.async = false; + } + } + + var self = this; + this.script.onreadystatechange = function() { + if (self.script && /loaded|complete/.test(self.script.readyState)) { + callback(true); + } + }; + + var head = document.getElementsByTagName('head')[0]; + head.insertBefore(this.script, head.firstChild); + if (this.errorScript) { + head.insertBefore(this.errorScript, this.script.nextSibling); + } + + return true; + }; + + prototype.cleanup = function() { + if (this.script && this.script.parentNode) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + if (this.errorScript && this.errorScript.parentNode) { + this.errorScript.parentNode.removeChild(this.errorScript); + this.errorScript = null; + } + }; + + function encodeData(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 JSONPReceiver() { + this.lastId = 0; + this.callbacks = {}; + } + + var prototype = JSONPReceiver.prototype; + + prototype.register = function(callback) { + this.lastId++; + var id = this.lastId; + this.callbacks[id] = callback; + return id; + }; + + prototype.unregister = function(id) { + if (this.callbacks[id]) { + var callback = this.callbacks[id]; + delete this.callbacks[id]; + return callback; + } else { + return null; + } + }; + + prototype.receive = function(id, error, data) { + var callback = this.unregister(id); + if (callback) { + callback(error, data); + } + }; + + Pusher.JSONPReceiver = JSONPReceiver; + Pusher.JSONP = new JSONPReceiver(); + +}).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 (this.options.level === undefined || level <= this.options.level) { + this.events.push( + Pusher.Util.extend({}, event, { + timestamp: Pusher.Util.now(), + level: (level !== Timeline.INFO ? level : undefined) + }) + ); + 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, + 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) { + if (this.timeline.isEmpty()) { + return; + } + + var self = this; + var scheme = "http" + (encrypted ? "s" : "") + "://"; + + var sendJSONP = function(data, callback) { + var params = { + data: Pusher.Util.filterObject(data, function(v) { + return v !== undefined; + }), + url: scheme + (self.host || self.options.host) + self.options.path, + receiver: Pusher.JSONP + }; + return Pusher.JSONPRequest.send(params, function(error, result) { + if (result && result.host) { + self.host = result.host; + } + if (callback) { + callback(error, result); + } + }); + }; + 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, + 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() { + /** Handles common logic for all transports. + * + * Transport is a low-level connection 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 support and its features. + * + * This is an abstract class, please do not instantiate it. + * + * 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 AbstractTransport(name, priority, key, options) { + Pusher.EventsDispatcher.call(this); + + this.name = name; + this.priority = priority; + this.key = key; + this.state = "new"; + this.timeline = options.timeline; + this.activityTimeout = options.activityTimeout; + this.id = this.timeline.generateUniqueID(); + + this.options = { + encrypted: Boolean(options.encrypted), + hostUnencrypted: options.hostUnencrypted, + hostEncrypted: options.hostEncrypted + }; + } + var prototype = AbstractTransport.prototype; + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Checks whether the transport handles ping/pong on itself. + * + * @return {Boolean} + */ + prototype.supportsPing = function() { + return false; + }; + + /** 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.resource) { + self.changeState("initializing"); + Pusher.Dependencies.load(self.resource, function() { + self.changeState("initialized"); + }); + } else { + self.changeState("initialized"); + } + }; + + /** Tries to establish a connection. + * + * @returns {Boolean} false if transport is in invalid state + */ + prototype.connect = function() { + if (this.socket || this.state !== "initialized") { + return false; + } + + var url = this.getURL(this.key, this.options); + try { + this.socket = this.createSocket(url); + } catch (e) { + var self = this; + Pusher.Util.defer(function() { + self.onError(e); + self.changeState("closed"); + }); + return false; + } + + this.bindListeners(); + + Pusher.debug("Connecting", { transport: this.name, url: url }); + this.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) { + if (this.state === "open") { + // Workaround for MobileSafari bug (see https://gist.github.com/2052006) + var self = this; + setTimeout(function() { + if (self.socket) { + self.socket.send(data); + } + }, 0); + return true; + } else { + return false; + } + }; + + /** @protected */ + prototype.onOpen = function() { + this.changeState("open"); + this.socket.onopen = undefined; + }; + + /** @protected */ + prototype.onError = function(error) { + this.emit("error", { type: 'WebSocketError', error: error }); + this.timeline.error(this.buildTimelineMessage({})); + }; + + /** @protected */ + prototype.onClose = function(closeEvent) { + if (closeEvent) { + this.changeState("closed", { + code: closeEvent.code, + reason: closeEvent.reason, + wasClean: closeEvent.wasClean + }); + } else { + this.changeState("closed"); + } + this.socket = undefined; + }; + + /** @protected */ + prototype.onMessage = function(message) { + this.emit("message", message); + }; + + /** @protected */ + prototype.bindListeners = function() { + var self = this; + + this.socket.onopen = function() { self.onOpen(); }; + this.socket.onerror = function(error) { self.onError(error); }; + this.socket.onclose = function(closeEvent) { self.onClose(closeEvent); }; + this.socket.onmessage = function(message) { self.onMessage(message); }; + }; + + /** @protected */ + prototype.createSocket = function(url) { + return null; + }; + + /** @protected */ + prototype.getScheme = function() { + return this.options.encrypted ? "wss" : "ws"; + }; + + /** @protected */ + prototype.getBaseURL = function() { + var host; + if (this.options.encrypted) { + host = this.options.hostEncrypted; + } else { + host = this.options.hostUnencrypted; + } + return this.getScheme() + "://" + host; + }; + + /** @protected */ + prototype.getPath = function() { + return "/app/" + this.key; + }; + + /** @protected */ + prototype.getQueryString = function() { + return "?protocol=" + Pusher.PROTOCOL + + "&client=js&version=" + Pusher.VERSION; + }; + + /** @protected */ + prototype.getURL = function() { + return this.getBaseURL() + this.getPath() + this.getQueryString(); + }; + + /** @protected */ + prototype.changeState = function(state, params) { + this.state = state; + this.timeline.info(this.buildTimelineMessage({ + state: state, + params: params + })); + this.emit(state, params); + }; + + /** @protected */ + prototype.buildTimelineMessage = function(message) { + return Pusher.Util.extend({ cid: this.id }, message); + }; + + Pusher.AbstractTransport = AbstractTransport; +}).call(this); + +;(function() { + /** Transport using Flash to emulate WebSockets. + * + * @see AbstractTransport + */ + function FlashTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + } + var prototype = FlashTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + prototype.resource = "flashfallback"; + + /** Creates a new instance of FlashTransport. + * + * @param {String} key + * @param {Object} options + * @return {FlashTransport} + */ + FlashTransport.createConnection = function(name, priority, key, options) { + return new FlashTransport(name, priority, key, options); + }; + + /** Checks whether Flash is supported in the browser. + * + * It is possible to disable flash by passing an envrionment object with the + * disableFlash property set to true. + * + * @see AbstractTransport.isSupported + * @param {Object} environment + * @returns {Boolean} + */ + FlashTransport.isSupported = function() { + try { + return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); + } catch (e) { + try { + return Boolean( + navigator && + navigator.mimeTypes && + navigator.mimeTypes["application/x-shockwave-flash"] !== undefined + ); + } catch(e) { + return false; + } + } + }; + + /** Fetches flashfallback dependency if needed. + * + * Sets WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR to true (if not set before) + * and WEB_SOCKET_SWF_LOCATION to Pusher's cdn before loading Flash resources. + * + * @see AbstractTransport.prototype.initialize + */ + prototype.initialize = 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"; + Pusher.AbstractTransport.prototype.initialize.call(this); + }; + + /** @protected */ + prototype.createSocket = function(url) { + return new FlashWebSocket(url); + }; + + /** @protected */ + prototype.getQueryString = function() { + return Pusher.AbstractTransport.prototype.getQueryString.call(this) + + "&flash=true"; + }; + + Pusher.FlashTransport = FlashTransport; +}).call(this); + +;(function() { + /** Abstract class for HTTP transports. + * + * @see AbstractTransport + */ + function AbstractHTTPTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + } + var prototype = AbstractHTTPTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + /** Always returns true, since all HTTP transports handle ping on their own. + * + * @returns {Boolean} always true + */ + prototype.supportsPing = function() { + return true; + }; + + /** @protected */ + prototype.getScheme = function() { + return this.options.encrypted ? "https" : "http"; + }; + + /** @protected */ + prototype.getPath = function() { + return (this.options.httpPath || "/pusher") + "/app/" + this.key; + }; + + Pusher.AbstractHTTPTransport = AbstractHTTPTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function XHRStreamingTransport(name, priority, key, options) { + Pusher.AbstractHTTPTransport.call(this, name, priority, key, options); + } + var prototype = XHRStreamingTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractHTTPTransport.prototype); + + prototype.resource = "xhr"; + + /** Creates a new instance of XHRStreamingTransport. + * + * @param {String} key + * @param {Object} options + * @return {XHRStreamingTransport} + */ + XHRStreamingTransport.createConnection = function(name, priority, key, options) { + return new XHRStreamingTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + XHRStreamingTransport.isSupported = function() { + return Pusher.Util.isXHRSupported(); + }; + + /** @protected */ + prototype.createSocket = function(url) { + return Pusher.HTTP.getStreamingSocket(url); + }; + + Pusher.XHRStreamingTransport = XHRStreamingTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function XDRStreamingTransport(name, priority, key, options) { + Pusher.XHRStreamingTransport.call(this, name, priority, key, options); + } + var prototype = XDRStreamingTransport.prototype; + Pusher.Util.extend(prototype, Pusher.XHRStreamingTransport.prototype); + + prototype.resource = "xdr"; + + /** Creates a new instance of XDRStreamingTransport. + * + * @param {String} key + * @param {Object} options + * @return {XDRStreamingTransport} + */ + XDRStreamingTransport.createConnection = function(name, priority, key, options) { + return new XDRStreamingTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + XDRStreamingTransport.isSupported = function(environment) { + return Pusher.Util.isXDRSupported(environment.encrypted); + }; + + Pusher.XDRStreamingTransport = XDRStreamingTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function XHRPollingTransport(name, priority, key, options) { + Pusher.AbstractHTTPTransport.call(this, name, priority, key, options); + } + var prototype = XHRPollingTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractHTTPTransport.prototype); + + prototype.resource = "xhr"; + + /** Creates a new instance of XHRPollingTransport. + * + * @param {String} key + * @param {Object} options + * @return {XHRPollingTransport} + */ + XHRPollingTransport.createConnection = function(name, priority, key, options) { + return new XHRPollingTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + XHRPollingTransport.isSupported = function() { + return Pusher.Util.isXHRSupported(); + }; + + /** @protected */ + prototype.createSocket = function(url) { + return Pusher.HTTP.getPollingSocket(url); + }; + + Pusher.XHRPollingTransport = XHRPollingTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function XDRPollingTransport(name, priority, key, options) { + Pusher.XHRPollingTransport.call(this, name, priority, key, options); + } + var prototype = XDRPollingTransport.prototype; + Pusher.Util.extend(prototype, Pusher.XHRPollingTransport.prototype); + + prototype.resource = "xdr"; + + /** Creates a new instance of XDRPollingTransport. + * + * @param {String} key + * @param {Object} options + * @return {XDRPollingTransport} + */ + XDRPollingTransport.createConnection = function(name, priority, key, options) { + return new XDRPollingTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + XDRPollingTransport.isSupported = function(environment) { + return Pusher.Util.isXDRSupported(environment.encrypted); + }; + + Pusher.XDRPollingTransport = XDRPollingTransport; +}).call(this); + +;(function() { + /** Fallback transport using SockJS. + * + * @see AbstractTransport + */ + function SockJSTransport(name, priority, key, options) { + Pusher.AbstractHTTPTransport.call(this, name, priority, key, options); + this.options.ignoreNullOrigin = options.ignoreNullOrigin; + } + var prototype = SockJSTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractHTTPTransport.prototype); + + prototype.resource = "sockjs"; + + /** Creates a new instance of SockJSTransport. + * + * @param {String} key + * @param {Object} options + * @return {SockJSTransport} + */ + SockJSTransport.createConnection = function(name, priority, key, options) { + return new SockJSTransport(name, priority, key, options); + }; + + /** Assumes that SockJS is always supported. + * + * @returns {Boolean} always true + */ + SockJSTransport.isSupported = function() { + return true; + }; + + /** @protected */ + prototype.createSocket = function(url) { + return new SockJS(url, null, { + js_path: Pusher.Dependencies.getPath("sockjs", { + encrypted: this.options.encrypted + }), + ignore_null_origin: this.options.ignoreNullOrigin + }); + }; + + /** @protected */ + prototype.getPath = function() { + return this.options.httpPath || "/pusher"; + }; + + /** @protected */ + prototype.getQueryString = function() { + return ""; + }; + + /** Handles opening a SockJS connection to Pusher. + * + * Since SockJS does not handle custom paths, we send it immediately after + * establishing the connection. + * + * @protected + */ + prototype.onOpen = function() { + this.socket.send(JSON.stringify({ + path: Pusher.AbstractTransport.prototype.getPath.call(this) + + Pusher.AbstractTransport.prototype.getQueryString.call(this) + })); + this.changeState("open"); + this.socket.onopen = undefined; + }; + + Pusher.SockJSTransport = SockJSTransport; +}).call(this); + +;(function() { + /** WebSocket transport. + * + * @see AbstractTransport + */ + function WSTransport(name, priority, key, options) { + Pusher.AbstractTransport.call(this, name, priority, key, options); + } + var prototype = WSTransport.prototype; + Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype); + + /** Creates a new instance of WSTransport. + * + * @param {String} key + * @param {Object} options + * @return {WSTransport} + */ + WSTransport.createConnection = function(name, priority, key, options) { + return new WSTransport(name, priority, key, options); + }; + + /** Checks whether the browser supports WebSockets in any form. + * + * @returns {Boolean} true if browser supports WebSockets + */ + WSTransport.isSupported = function() { + return window.WebSocket !== undefined || window.MozWebSocket !== undefined; + }; + + /** @protected */ + prototype.createSocket = function(url) { + var constructor = window.WebSocket || window.MozWebSocket; + return new constructor(url); + }; + + /** @protected */ + prototype.getQueryString = function() { + return Pusher.AbstractTransport.prototype.getQueryString.call(this) + + "&flash=false"; + }; + + Pusher.WSTransport = WSTransport; +}).call(this); + +;(function() { + 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; + + prototype.createConnection = function(name, priority, key, options) { + var self = this; + + var 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; + }; + + prototype.isSupported = function(environment) { + return this.manager.isAlive() && this.transport.isSupported(environment); + }; + + Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager; +}).call(this); + +;(function() { + function TransportManager(options) { + this.options = options || {}; + this.livesLeft = this.options.lives || Infinity; + } + var prototype = TransportManager.prototype; + + prototype.getAssistant = function(transport) { + return new Pusher.AssistantToTheTransportManager(this, transport, { + minPingDelay: this.options.minPingDelay, + maxPingDelay: this.options.maxPingDelay + }); + }; + + prototype.isAlive = function() { + return this.livesLeft > 0; + }; + + 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 = { + 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 ping/pong by itself + * + * @returns {Boolean} true if ping is handled by the transport + */ + prototype.supportsPing = function() { + return this.transport.supportsPing(); + }; + + /** 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)); + }; + + /** Closes the connection. */ + prototype.close = function() { + this.transport.close(); + }; + + /** @private */ + prototype.bindListeners = function() { + var self = this; + + var onMessage = 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); + } + }; + var onError = function(error) { + self.emit("error", { type: "WebSocketError", error: error }); + }; + var onClosed = function(closeEvent) { + unbindListeners(); + + if (closeEvent && closeEvent.code) { + self.handleCloseEvent(closeEvent); + } + + self.transport = null; + self.emit("closed"); + }; + + var unbindListeners = function() { + self.transport.unbind("closed", onClosed); + self.transport.unbind("error", onError); + self.transport.unbind("message", onMessage); + }; + + self.transport.bind("message", onMessage); + self.transport.bind("error", onError); + self.transport.bind("closed", onClosed); + }; + + /** @private */ + prototype.handleCloseEvent = function(closeEvent) { + var action = Pusher.Protocol.getCloseAction(closeEvent); + var error = Pusher.Protocol.getCloseError(closeEvent); + if (error) { + this.emit('error', error); + } + if (action) { + this.emit(action); + } + }; + + Pusher.Connection = Connection; +}).call(this); + +;(function() { + /** + * Handles Pusher protocol handshakes for transports. + * + * Calls back with a result object after handshake is completed. Results + * always have two fields: + * - action - string describing action to be taken after the handshake + * - transport - the transport object passed to the constructor + * + * Different actions can set different additional properties on the result. + * In the case of 'connected' action, there will be a 'connection' property + * containing a Connection object for the transport. Other actions should + * carry an 'error' property. + * + * @param {AbstractTransport} transport + * @param {Function} callback + */ + function Handshake(transport, callback) { + this.transport = transport; + this.callback = callback; + this.bindListeners(); + } + var prototype = Handshake.prototype; + + prototype.close = function() { + this.unbindListeners(); + this.transport.close(); + }; + + /** @private */ + prototype.bindListeners = function() { + var self = this; + + self.onMessage = function(m) { + self.unbindListeners(); + + try { + var result = Pusher.Protocol.processHandshake(m); + if (result.action === "connected") { + self.finish("connected", { + connection: new Pusher.Connection(result.id, self.transport), + activityTimeout: result.activityTimeout + }); + } else { + self.finish(result.action, { error: result.error }); + self.transport.close(); + } + } catch (e) { + self.finish("error", { error: e }); + self.transport.close(); + } + }; + + self.onClosed = function(closeEvent) { + self.unbindListeners(); + + var action = Pusher.Protocol.getCloseAction(closeEvent) || "backoff"; + var error = Pusher.Protocol.getCloseError(closeEvent); + self.finish(action, { error: error }); + }; + + self.transport.bind("message", self.onMessage); + self.transport.bind("closed", self.onClosed); + }; + + /** @private */ + prototype.unbindListeners = function() { + this.transport.unbind("message", this.onMessage); + this.transport.unbind("closed", this.onClosed); + }; + + /** @private */ + prototype.finish = function(action, params) { + this.callback( + Pusher.Util.extend({ transport: this.transport, action: action }, params) + ); + }; + + Pusher.Handshake = Handshake; +}).call(this); + +;(function() { + /** Manages connection to Pusher. + * + * Uses a strategy (currently only default), timers and network availability + * info to establish a connection and export its state. In case of failures, + * manages reconnection attempts. + * + * Exports state changes as following events: + * - "state_change", { previous: p, current: state } + * - state + * + * States: + * - initialized - initial state, never transitioned to + * - connecting - connection is being established + * - connected - connection has been fully established + * - disconnected - on requested disconnection + * - unavailable - after connection timeout or when there's no network + * - failed - when the connection strategy is not supported + * + * Options: + * - unavailableTimeout - time to transition to unavailable state + * - activityTimeout - time after which ping message should be sent + * - pongTimeout - time for Pusher to respond with pong before reconnecting + * + * @param {String} key application key + * @param {Object} options + */ + function ConnectionManager(key, options) { + Pusher.EventsDispatcher.call(this); + + this.key = key; + this.options = options || {}; + this.state = "initialized"; + this.connection = null; + this.encrypted = !!options.encrypted; + this.timeline = this.options.timeline; + + this.connectionCallbacks = this.buildConnectionCallbacks(); + this.errorCallbacks = this.buildErrorCallbacks(); + this.handshakeCallbacks = this.buildHandshakeCallbacks(this.errorCallbacks); + + var self = this; + + Pusher.Network.bind("online", function() { + self.timeline.info({ netinfo: "online" }); + if (self.state === "connecting" || self.state === "unavailable") { + self.retryIn(0); + } + }); + Pusher.Network.bind("offline", function() { + self.timeline.info({ netinfo: "offline" }); + if (self.state === "connected") { + self.sendActivityCheck(); + } + }); + + this.updateStrategy(); + } + var prototype = ConnectionManager.prototype; + + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Establishes a connection to Pusher. + * + * Does nothing when connection is already established. See top-level doc + * to find events emitted on connection attempts. + */ + prototype.connect = function() { + if (this.connection || this.runner) { + return; + } + if (!this.strategy.isSupported()) { + this.updateState("failed"); + return; + } + this.updateState("connecting"); + this.startConnecting(); + this.setUnavailableTimer(); + }; + + /** Sends raw data. + * + * @param {String} data + */ + prototype.send = function(data) { + if (this.connection) { + return this.connection.send(data); + } else { + return false; + } + }; + + /** Sends an event. + * + * @param {String} name + * @param {String} data + * @param {String} [channel] + * @returns {Boolean} whether message was sent or not + */ + prototype.send_event = function(name, data, channel) { + if (this.connection) { + return this.connection.send_event(name, data, channel); + } else { + return false; + } + }; + + /** Closes the connection. */ + prototype.disconnect = function() { + this.disconnectInternally(); + this.updateState("disconnected"); + }; + + prototype.isEncrypted = function() { + return this.encrypted; + }; + + /** @private */ + prototype.startConnecting = function() { + var self = this; + var callback = function(error, handshake) { + if (error) { + self.runner = self.strategy.connect(0, callback); + } else { + if (handshake.action === "error") { + self.emit("error", { type: "HandshakeError", error: handshake.error }); + self.timeline.error({ handshakeError: handshake.error }); + } else { + self.abortConnecting(); // we don't support switching connections yet + self.handshakeCallbacks[handshake.action](handshake); + } + } + }; + self.runner = self.strategy.connect(0, callback); + }; + + /** @private */ + prototype.abortConnecting = function() { + if (this.runner) { + this.runner.abort(); + this.runner = null; + } + }; + + /** @private */ + prototype.disconnectInternally = function() { + this.abortConnecting(); + this.clearRetryTimer(); + this.clearUnavailableTimer(); + this.stopActivityCheck(); + if (this.connection) { + var connection = this.abandonConnection(); + connection.close(); + } + }; + + /** @private */ + prototype.updateStrategy = function() { + this.strategy = this.options.getStrategy({ + key: this.key, + timeline: this.timeline, + encrypted: this.encrypted + }); + }; + + /** @private */ + prototype.retryIn = function(delay) { + var self = this; + self.timeline.info({ action: "retry", delay: delay }); + if (delay > 0) { + self.emit("connecting_in", Math.round(delay / 1000)); + } + self.retryTimer = new Pusher.Timer(delay || 0, function() { + self.disconnectInternally(); + self.connect(); + }); + }; + + /** @private */ + prototype.clearRetryTimer = function() { + if (this.retryTimer) { + this.retryTimer.ensureAborted(); + this.retryTimer = null; + } + }; + + /** @private */ + prototype.setUnavailableTimer = function() { + var self = this; + self.unavailableTimer = new Pusher.Timer( + self.options.unavailableTimeout, + function() { + self.updateState("unavailable"); + } + ); + }; + + /** @private */ + prototype.clearUnavailableTimer = function() { + if (this.unavailableTimer) { + this.unavailableTimer.ensureAborted(); + } + }; + + /** @private */ + prototype.sendActivityCheck = function() { + var self = this; + self.stopActivityCheck(); + self.send_event('pusher: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.supportsPing()) { + 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', {}); + }, + error: function(error) { + // just emit error to user - socket will already be closed by browser + self.emit("error", { type: "WebSocketError", error: error }); + }, + closed: function() { + self.abandonConnection(); + if (self.shouldRetry()) { + self.retryIn(1000); + } + } + }; + }; + + /** @private */ + prototype.buildHandshakeCallbacks = function(errorCallbacks) { + var self = this; + return Pusher.Util.extend({}, errorCallbacks, { + connected: function(handshake) { + self.activityTimeout = Math.min( + self.options.activityTimeout, + handshake.activityTimeout, + handshake.connection.activityTimeout || Infinity + ); + self.clearUnavailableTimer(); + self.setConnection(handshake.connection); + self.socket_id = self.connection.id; + self.updateState("connected", { socket_id: self.socket_id }); + } + }); + }; + + /** @private */ + prototype.buildErrorCallbacks = function() { + var self = this; + + function withErrorEmitted(callback) { + return function(result) { + if (result.error) { + self.emit("error", { type: "WebSocketError", error: result.error }); + } + callback(result); + }; + } + + return { + ssl_only: withErrorEmitted(function() { + self.encrypted = true; + self.updateStrategy(); + self.retryIn(0); + }), + refused: withErrorEmitted(function() { + self.disconnect(); + }), + backoff: withErrorEmitted(function() { + self.retryIn(1000); + }), + retry: withErrorEmitted(function() { + self.retryIn(0); + }) + }; + }; + + /** @private */ + prototype.setConnection = function(connection) { + this.connection = connection; + for (var event in this.connectionCallbacks) { + this.connection.bind(event, this.connectionCallbacks[event]); + } + this.resetActivityCheck(); + }; + + /** @private */ + prototype.abandonConnection = function() { + if (!this.connection) { + return; + } + for (var event in this.connectionCallbacks) { + this.connection.unbind(event, this.connectionCallbacks[event]); + } + var connection = this.connection; + this.connection = null; + return connection; + }; + + /** @private */ + prototype.updateState = function(newState, data) { + var previousState = this.state; + this.state = newState; + if (previousState !== newState) { + Pusher.debug('State changed', previousState + ' -> ' + newState); + this.timeline.info({ state: newState, params: data }); + this.emit('state_change', { previous: previousState, current: newState }); + this.emit(newState, data); + } + }; + + /** @private */ + prototype.shouldRetry = function() { + return this.state === "connecting" || this.state === "connected"; + }; + + Pusher.ConnectionManager = ConnectionManager; +}).call(this); + +;(function() { + /** Really basic interface providing network availability info. + * + * Emits: + * - online - when browser goes online + * - offline - when browser goes offline + */ + function NetInfo() { + Pusher.EventsDispatcher.call(this); + + var self = this; + // This is okay, as IE doesn't support this stuff anyway. + if (window.addEventListener !== undefined) { + window.addEventListener("online", function() { + self.emit('online'); + }, false); + window.addEventListener("offline", function() { + self.emit('offline'); + }, false); + } + } + Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype); + + var prototype = NetInfo.prototype; + + /** Returns whether browser is online or not + * + * Offline means definitely offline (no connection to router). + * Inverse does NOT mean definitely online (only currently supported in Safari + * and even there only means the device has a connection to the router). + * + * @return {Boolean} + */ + prototype.isOnline = function() { + if (window.navigator.onLine === undefined) { + return true; + } else { + return window.navigator.onLine; + } + }; + + Pusher.NetInfo = NetInfo; + Pusher.Network = new NetInfo(); +}).call(this); + +;(function() { + /** Represents a collection of members of a presence channel. */ + function Members() { + this.reset(); + } + var prototype = Members.prototype; + + /** Returns member's info for given id. + * + * Resulting object containts two fields - id and info. + * + * @param {Number} id + * @return {Object} member's info or null + */ + prototype.get = function(id) { + if (Object.prototype.hasOwnProperty.call(this.members, id)) { + return { + id: id, + info: this.members[id] + }; + } else { + return null; + } + }; + + /** Calls back for each member in unspecified order. + * + * @param {Function} callback + */ + prototype.each = function(callback) { + var self = this; + Pusher.Util.objectApply(self.members, function(member, id) { + callback(self.get(id)); + }); + }; + + /** Updates the id for connected member. For internal use only. */ + prototype.setMyID = function(id) { + this.myID = id; + }; + + /** Handles subscription data. For internal use only. */ + prototype.onSubscription = function(subscriptionData) { + this.members = subscriptionData.presence.hash; + this.count = subscriptionData.presence.count; + this.me = this.get(this.myID); + }; + + /** Adds a new member to the collection. For internal use only. */ + prototype.addMember = function(memberData) { + if (this.get(memberData.user_id) === null) { + this.count++; + } + this.members[memberData.user_id] = memberData.user_info; + return this.get(memberData.user_id); + }; + + /** Adds a member from the collection. For internal use only. */ + prototype.removeMember = function(memberData) { + var member = this.get(memberData.user_id); + if (member) { + delete this.members[memberData.user_id]; + this.count--; + } + return member; + }; + + /** Resets the collection to the initial state. For internal use only. */ + prototype.reset = function() { + this.members = {}; + this.count = 0; + this.myID = null; + this.me = null; + }; + + Pusher.Members = Members; +}).call(this); + +;(function() { + /** Provides base public channel interface with an event emitter. + * + * Emits: + * - pusher:subscription_succeeded - after subscribing successfully + * - other non-internal events + * + * @param {String} name + * @param {Pusher} pusher + */ + function Channel(name, pusher) { + Pusher.EventsDispatcher.call(this, function(event, data) { + Pusher.debug('No callbacks on ' + name + ' for ' + event); + }); + + this.name = name; + this.pusher = pusher; + this.subscribed = false; + } + var prototype = Channel.prototype; + Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype); + + /** Skips authorization, since public channels don't require it. + * + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + return callback(false, {}); + }; + + /** Triggers an event */ + prototype.trigger = function(event, data) { + if (event.indexOf("client-") !== 0) { + throw new Pusher.Errors.BadEventName( + "Event '" + event + "' does not start with 'client-'" + ); + } + return this.pusher.send_event(event, data, this.name); + }; + + /** Signals disconnection to the channel. For internal use only. */ + prototype.disconnect = function() { + this.subscribed = false; + }; + + /** Handles an event. For internal use only. + * + * @param {String} event + * @param {*} data + */ + prototype.handleEvent = function(event, data) { + if (event.indexOf("pusher_internal:") === 0) { + if (event === "pusher_internal:subscription_succeeded") { + this.subscribed = true; + this.emit("pusher:subscription_succeeded", data); + } + } else { + this.emit(event, data); + } + }; + + /** Sends a subscription request. For internal use only. */ + prototype.subscribe = function() { + var self = this; + + self.authorize(self.pusher.connection.socket_id, function(error, data) { + if (error) { + self.handleEvent('pusher:subscription_error', data); + } else { + self.pusher.send_event('pusher:subscribe', { + auth: data.auth, + channel_data: data.channel_data, + channel: self.name + }); + } + }); + }; + + /** Sends an unsubscription request. For internal use only. */ + prototype.unsubscribe = function() { + this.pusher.send_event('pusher:unsubscribe', { + channel: this.name + }); + }; + + Pusher.Channel = Channel; +}).call(this); + +;(function() { + /** Extends public channels to provide private channel interface. + * + * @param {String} name + * @param {Pusher} pusher + */ + function PrivateChannel(name, pusher) { + Pusher.Channel.call(this, name, pusher); + } + var prototype = PrivateChannel.prototype; + Pusher.Util.extend(prototype, Pusher.Channel.prototype); + + /** Authorizes the connection to use the channel. + * + * @param {String} socketId + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + var authorizer = new Pusher.Channel.Authorizer(this, this.pusher.config); + return authorizer.authorize(socketId, callback); + }; + + Pusher.PrivateChannel = PrivateChannel; +}).call(this); + +;(function() { + /** Adds presence channel functionality to private channels. + * + * @param {String} name + * @param {Pusher} pusher + */ + function PresenceChannel(name, pusher) { + Pusher.PrivateChannel.call(this, name, pusher); + this.members = new Pusher.Members(); + } + var prototype = PresenceChannel.prototype; + Pusher.Util.extend(prototype, Pusher.PrivateChannel.prototype); + + /** Authenticates the connection as a member of the channel. + * + * @param {String} socketId + * @param {Function} callback + */ + prototype.authorize = function(socketId, callback) { + var _super = Pusher.PrivateChannel.prototype.authorize; + var self = this; + _super.call(self, socketId, function(error, authData) { + if (!error) { + if (authData.channel_data === undefined) { + Pusher.warn( + "Invalid auth response for channel '" + + self.name + + "', expected 'channel_data' field" + ); + callback("Invalid auth response"); + return; + } + var channelData = JSON.parse(authData.channel_data); + self.members.setMyID(channelData.user_id); + } + callback(error, authData); + }); + }; + + /** Handles presence and subscription events. For internal use only. + * + * @param {String} event + * @param {*} data + */ + prototype.handleEvent = function(event, data) { + switch (event) { + case "pusher_internal:subscription_succeeded": + this.members.onSubscription(data); + this.subscribed = true; + this.emit("pusher:subscription_succeeded", this.members); + break; + case "pusher_internal:member_added": + var addedMember = this.members.addMember(data); + this.emit('pusher:member_added', addedMember); + break; + case "pusher_internal:member_removed": + var removedMember = this.members.removeMember(data); + if (removedMember) { + this.emit('pusher:member_removed', removedMember); + } + break; + default: + Pusher.PrivateChannel.prototype.handleEvent.call(this, event, data); + } + }; + + /** Resets the channel state, including members map. For internal use only. */ + prototype.disconnect = function() { + this.members.reset(); + Pusher.PrivateChannel.prototype.disconnect.call(this); + }; + + Pusher.PresenceChannel = PresenceChannel; +}).call(this); + +;(function() { + /** Handles a channel map. */ + function Channels() { + this.channels = {}; + } + var prototype = Channels.prototype; + + /** Creates or retrieves an existing channel by its name. + * + * @param {String} name + * @param {Pusher} pusher + * @return {Channel} + */ + prototype.add = function(name, pusher) { + if (!this.channels[name]) { + this.channels[name] = createChannel(name, pusher); + } + return this.channels[name]; + }; + + /** Returns a list of all channels + * + * @return {Array} + */ + prototype.all = function(name) { + return Pusher.Util.values(this.channels); + }; + + /** Finds a channel by its name. + * + * @param {String} name + * @return {Channel} channel or null if it doesn't exist + */ + prototype.find = function(name) { + return this.channels[name]; + }; + + /** Removes a channel from the map. + * + * @param {String} name + */ + prototype.remove = function(name) { + var channel = this.channels[name]; + delete this.channels[name]; + return channel; + }; + + /** Proxies disconnection signal to all channels. */ + prototype.disconnect = function() { + Pusher.Util.objectApply(this.channels, function(channel) { + channel.disconnect(); + }); + }; + + function createChannel(name, pusher) { + if (name.indexOf('private-') === 0) { + return new Pusher.PrivateChannel(name, pusher); + } else if (name.indexOf('presence-') === 0) { + return new Pusher.PresenceChannel(name, pusher); + } else { + return new Pusher.Channel(name, pusher); + } + } + + Pusher.Channels = Channels; +}).call(this); + +;(function() { + Pusher.Channel.Authorizer = function(channel, options) { + this.channel = channel; + this.type = options.authTransport; + + this.options = options; + this.authOptions = (options || {}).auth || {}; + }; + + Pusher.Channel.Authorizer.prototype = { + composeQuery: function(socketId) { + var query = '&socket_id=' + encodeURIComponent(socketId) + + '&channel_name=' + encodeURIComponent(this.channel.name); + + for(var i in this.authOptions.params) { + query += "&" + encodeURIComponent(i) + "=" + encodeURIComponent(this.authOptions.params[i]); + } + + return query; + }, + + authorize: function(socketId, callback) { + return Pusher.authorizers[this.type].call(this, socketId, callback); + } + }; + + var nextAuthCallbackID = 1; + + Pusher.auth_callbacks = {}; + Pusher.authorizers = { + ajax: function(socketId, callback){ + var self = this, xhr; + + if (Pusher.XHR) { + xhr = new Pusher.XHR(); + } else { + xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP")); + } + + xhr.open("POST", self.options.authEndpoint, true); + + // add request headers + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + for(var headerName in this.authOptions.headers) { + xhr.setRequestHeader(headerName, this.authOptions.headers[headerName]); + } + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var data, parsed = false; + + try { + data = JSON.parse(xhr.responseText); + parsed = true; + } catch (e) { + callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText); + } + + if (parsed) { // prevents double execution. + callback(false, data); + } + } else { + Pusher.warn("Couldn't get auth info from your webapp", xhr.status); + callback(true, xhr.status); + } + } + }; + + xhr.send(this.composeQuery(socketId)); + return xhr; + }, + + jsonp: function(socketId, callback){ + if(this.authOptions.headers !== undefined) { + Pusher.warn("Warn", "To send headers with the auth request, you must use AJAX, rather than JSONP."); + } + + var callbackName = nextAuthCallbackID.toString(); + nextAuthCallbackID++; + + var document = Pusher.Util.getDocument(); + var script = document.createElement("script"); + // Hacked wrapper. + Pusher.auth_callbacks[callbackName] = function(data) { + callback(false, data); + }; + + var callback_name = "Pusher.auth_callbacks['" + callbackName + "']"; + script.src = this.options.authEndpoint + + '?callback=' + + encodeURIComponent(callback_name) + + this.composeQuery(socketId); + + var head = document.getElementsByTagName("head")[0] || document.documentElement; + head.insertBefore( script, head.firstChild ); + } + }; +}).call(this); +