diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 5f07a0fae..13a2ca37c 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,5 +1,19 @@ # Changelog +## 2.1.5 (2013-12-16) + +[NEW] Server can suggest a lower activity timeout in the handshake + +[CHANGED] Updated the protocol to version 7 + +[CHANGED] Transports are cached separately for encrypted connections + +[CHANGED] Updated the stats protocol + +[FIXED] Removed the `Protocol` variable leaking into the global scope + +[FIXED] Flash check was occasionally raising exceptions on old Firefox releases + ## 2.1.4 (2013-11-26) [NEW] Added the `Pusher.prototype.allChannels` method diff --git a/JFile b/JFile index bf3aa7ecb..29b1f38f0 100644 --- a/JFile +++ b/JFile @@ -7,7 +7,7 @@ target_dir './dist' src_dir './src' -version '2.1.4' +version '2.1.5' bundle 'pusher.js' do license 'pusher-licence.js' diff --git a/dist/flashfallback.js b/dist/flashfallback.js index 59f2bb991..47e5349d7 100644 --- a/dist/flashfallback.js +++ b/dist/flashfallback.js @@ -1,5 +1,5 @@ /*! - * Pusher JavaScript Library v2.1.4 + * Pusher JavaScript Library v2.1.5 * http://pusherapp.com/ * * Copyright 2011, Pusher diff --git a/dist/flashfallback.min.js b/dist/flashfallback.min.js index 4dbf71023..d4895aa87 100644 --- a/dist/flashfallback.min.js +++ b/dist/flashfallback.min.js @@ -1,5 +1,5 @@ /*! - * Pusher JavaScript Library v2.1.4 + * Pusher JavaScript Library v2.1.5 * http://pusherapp.com/ * * Copyright 2011, Pusher diff --git a/dist/pusher.js b/dist/pusher.js index bb35cae6e..27f779211 100644 --- a/dist/pusher.js +++ b/dist/pusher.js @@ -1,5 +1,5 @@ /*! - * Pusher JavaScript Library v2.1.4 + * Pusher JavaScript Library v2.1.5 * http://pusherapp.com/ * * Copyright 2013, Pusher @@ -86,6 +86,7 @@ }); Pusher.instances.push(this); + this.timeline.info({ instances: Pusher.instances.length }); if (Pusher.isReady) self.connect(); } @@ -598,8 +599,8 @@ }).call(this); ;(function() { - Pusher.VERSION = '2.1.4'; - Pusher.PROTOCOL = 6; + Pusher.VERSION = '2.1.5'; + Pusher.PROTOCOL = 7; // DEPRECATED: WS connection parameters Pusher.host = 'ws.pusherapp.com'; @@ -1186,22 +1187,15 @@ prototype.send = function(sendJSONP, callback) { var self = this; - var data = { + 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 - }; - if (self.sent === 0) { - Pusher.Util.extend(data, { - key: self.key, - features: self.options.features, - lib: "js", - version: self.options.version - }, self.options.params || {}); - } - data = Pusher.Util.filterObject(data, function(v) { - return v !== undefined; - }); + }, self.options.params); self.events = []; sendJSONP(data, function(error, result) { @@ -1241,7 +1235,9 @@ var sendJSONP = function(data, callback) { var params = { - data: data, + data: Pusher.Util.filterObject(data, function(v) { + return v !== undefined; + }), url: scheme + (self.host || self.options.host) + self.options.path, receiver: Pusher.JSONP }; @@ -1346,6 +1342,7 @@ this.strategy = strategy; this.transports = transports; this.ttl = options.ttl || 1800*1000; + this.encrypted = options.encrypted; this.timeline = options.timeline; } var prototype = CachedStrategy.prototype; @@ -1355,7 +1352,8 @@ }; prototype.connect = function(minPriority, callback) { - var info = fetchTransportInfo(); + var encrypted = this.encrypted; + var info = fetchTransportCache(encrypted); var strategies = [this.strategy]; if (info && info.timestamp + this.ttl >= Pusher.Util.now()) { @@ -1374,7 +1372,7 @@ minPriority, function cb(error, handshake) { if (error) { - flushTransportInfo(); + flushTransportCache(encrypted); if (strategies.length > 0) { startTimestamp = Pusher.Util.now(); runner = strategies.pop().connect(minPriority, cb); @@ -1382,8 +1380,11 @@ callback(error); } } else { - var latency = Pusher.Util.now() - startTimestamp; - storeTransportInfo(handshake.transport.name, latency); + storeTransportCache( + encrypted, + handshake.transport.name, + Pusher.Util.now() - startTimestamp + ); callback(null, handshake); } } @@ -1402,26 +1403,30 @@ }; }; - function fetchTransportInfo() { + function getTransportCacheKey(encrypted) { + return "pusherTransport" + (encrypted ? "Encrypted" : "Unencrypted"); + } + + function fetchTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { - var info = storage.pusherTransport; - if (info) { - return JSON.parse(info); + var serializedCache = storage[getTransportCacheKey(encrypted)]; + if (serializedCache) { + return JSON.parse(serializedCache); } } catch (e) { - flushTransportInfo(); + flushTransportCache(encrypted); } } return null; } - function storeTransportInfo(transport, latency) { + function storeTransportCache(encrypted, transport, latency) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { - storage.pusherTransport = JSON.stringify({ + storage[getTransportCacheKey(encrypted)] = JSON.stringify({ timestamp: Pusher.Util.now(), transport: transport, latency: latency @@ -1432,13 +1437,13 @@ } } - function flushTransportInfo() { + function flushTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); - if (storage && storage.pusherTransport) { + if (storage) { try { - delete storage.pusherTransport; + delete storage[getTransportCacheKey(encrypted)]; } catch (e) { - storage.pusherTransport = undefined; + // catch exceptions raised by localStorage } } } @@ -1825,6 +1830,7 @@ this.key = key; this.state = "new"; this.timeline = options.timeline; + this.activityTimeout = options.activityTimeout; this.id = this.timeline.generateUniqueID(); this.options = { @@ -1938,10 +1944,6 @@ } }; - prototype.requestPing = function() { - this.emit("ping_request"); - }; - /** @protected */ prototype.onOpen = function() { this.changeState("open"); @@ -2073,11 +2075,15 @@ try { return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); } catch (e) { - return Boolean( - navigator && - navigator.mimeTypes && - navigator.mimeTypes["application/x-shockwave-flash"] !== undefined - ); + try { + return Boolean( + navigator && + navigator.mimeTypes && + navigator.mimeTypes["application/x-shockwave-flash"] !== undefined + ); + } catch(e) { + return false; + } } }; @@ -2271,39 +2277,29 @@ this.transport = transport; this.minPingDelay = options.minPingDelay; this.maxPingDelay = options.maxPingDelay; - this.pingDelay = null; + this.pingDelay = undefined; } var prototype = AssistantToTheTransportManager.prototype; prototype.createConnection = function(name, priority, key, options) { - var connection = this.transport.createConnection( + var self = this; + + var options = Pusher.Util.extend({}, options, { + activityTimeout: self.pingDelay + }); + var connection = self.transport.createConnection( name, priority, key, options ); - var self = this; var openTimestamp = null; - var pingTimer = null; var onOpen = function() { connection.unbind("open", onOpen); - - openTimestamp = Pusher.Util.now(); - if (self.pingDelay) { - pingTimer = setInterval(function() { - if (pingTimer) { - connection.requestPing(); - } - }, self.pingDelay); - } - connection.bind("closed", onClosed); + openTimestamp = Pusher.Util.now(); }; var onClosed = function(closeEvent) { connection.unbind("closed", onClosed); - if (pingTimer) { - clearInterval(pingTimer); - pingTimer = null; - } if (closeEvent.code === 1002 || closeEvent.code === 1003) { // we don't want to use transports not obeying the protocol @@ -2554,7 +2550,7 @@ /** * Provides functions for handling Pusher protocol-specific messages. */ - Protocol = {}; + var Protocol = {}; /** * Decodes a message in a Pusher format. @@ -2611,7 +2607,11 @@ message = this.decodeMessage(message); if (message.event === "pusher:connection_established") { - return { action: "connected", id: message.data.socket_id }; + 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 @@ -2708,6 +2708,7 @@ this.id = id; this.transport = transport; + this.activityTimeout = transport.activityTimeout; this.bindListeners(); } var prototype = Connection.prototype; @@ -2783,9 +2784,6 @@ self.emit('message', message); } }; - var onPingRequest = function() { - self.emit("ping_request"); - }; var onError = function(error) { self.emit("error", { type: "WebSocketError", error: error }); }; @@ -2803,12 +2801,10 @@ var unbindListeners = function() { self.transport.unbind("closed", onClosed); self.transport.unbind("error", onError); - self.transport.unbind("ping_request", onPingRequest); self.transport.unbind("message", onMessage); }; self.transport.bind("message", onMessage); - self.transport.bind("ping_request", onPingRequest); self.transport.bind("error", onError); self.transport.bind("closed", onClosed); }; @@ -2868,7 +2864,8 @@ var result = Pusher.Protocol.processHandshake(m); if (result.action === "connected") { self.finish("connected", { - connection: new Pusher.Connection(result.id, self.transport) + connection: new Pusher.Connection(result.id, self.transport), + activityTimeout: result.activityTimeout }); } else { self.finish(result.action, { error: result.error }); @@ -3115,7 +3112,7 @@ if (!this.connection.supportsPing()) { var self = this; self.activityTimer = new Pusher.Timer( - self.options.activityTimeout, + self.activityTimeout, function() { self.send_event('pusher:ping', {}); // wait for pong response @@ -3150,9 +3147,6 @@ ping: function() { self.send_event('pusher:pong', {}); }, - ping_request: function() { - self.send_event('pusher:ping', {}); - }, error: function(error) { // just emit error to user - socket will already be closed by browser self.emit("error", { type: "WebSocketError", error: error }); @@ -3171,6 +3165,11 @@ 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; diff --git a/dist/pusher.min.js b/dist/pusher.min.js index 1b0439923..60c845ae3 100644 --- a/dist/pusher.min.js +++ b/dist/pusher.min.js @@ -1,5 +1,5 @@ /*! - * Pusher JavaScript Library v2.1.4 + * Pusher JavaScript Library v2.1.5 * http://pusherapp.com/ * * Copyright 2013, Pusher @@ -9,17 +9,17 @@ (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 d=a.event.indexOf("pusher_internal:")===0;if(a.channel){var b=c.channel(a.channel);b&&b.handleEvent(a.event,a.data)}d||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);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;athis.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={session:c.session,bundle:c.sent+1,timeline:c.events};c.sent===0&&Pusher.Util.extend(e,{key:c.key,features:c.options.features,lib:"js",version:c.options.version},c.options.params||{});e=Pusher.Util.filterObject(e,function(a){return a!==void 0});c.events=[];a(e,function(a,g){a||c.sent++;b&&b(a,g)});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,h="http"+(b?"s":"")+"://";d.timeline.send(function(a,b){return Pusher.JSONPRequest.send({data:a,url:h+(d.host||d.options.host)+d.options.path,receiver:Pusher.JSONP},function(a,c){if(c&&c.host)d.host=c.host;b&&b(a,c)})},a)}};Pusher.TimelineSender=b}).call(this); +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,g){a||c.sent++;b&&b(a,g)});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,h="http"+(b?"s":"")+"://";d.timeline.send(function(a,b){var c={data:Pusher.Util.filterObject(a,function(a){return a!==void 0}),url:h+(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 h=Pusher.Util.map(a,function(a,d,h,e){return a.connect(b,c(d,e))});return{abort:function(){Pusher.Util.apply(h,d)},forceMinPriority:function(a){Pusher.Util.apply(h,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 h=b.prototype;h.isSupported=function(){return Pusher.Util.any(this.strategies,Pusher.Util.method("isSupported"))}; h.connect=function(b,d){return c(this.strategies,b,function(b,c){return function(h,e){(c[b].error=h)?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.timeline=c.timeline}function c(){var b=Pusher.Util.getLocalStorage();if(b)try{var c=b.pusherTransport;if(c)return JSON.parse(c)}catch(d){a()}return null}function a(){var a=Pusher.Util.getLocalStorage();if(a&&a.pusherTransport)try{delete a.pusherTransport}catch(b){a.pusherTransport=void 0}}var d=b.prototype;d.isSupported=function(){return this.strategy.isSupported()};d.connect=function(b,d){var f=c(),g=[this.strategy]; -if(f&&f.timestamp+this.ttl>=Pusher.Util.now()){var i=this.transports[f.transport];i&&(this.timeline.info({cached:!0,transport:f.transport}),g.push(new Pusher.SequentialStrategy([i],{timeout:f.latency*2,failFast:!0})))}var j=Pusher.Util.now(),k=g.pop().connect(b,function n(c,i){if(c)a(),g.length>0?(j=Pusher.Util.now(),k=g.pop().connect(b,n)):d(c);else{var f=Pusher.Util.now()-j,p=i.transport.name,o=Pusher.Util.getLocalStorage();if(o)try{o.pusherTransport=JSON.stringify({timestamp:Pusher.Util.now(), -transport:p,latency:f})}catch(q){}d(null,i)}});return{abort:function(){k.abort()},forceMinPriority:function(a){b=a;k&&k.forceMinPriority(a)}}};Pusher.CachedStrategy=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 h=b[c(a)];if(h)return JSON.parse(h)}catch(i){d(a)}return null}function d(a){var b=Pusher.Util.getLocalStorage();if(b)try{delete b[c(a)]}catch(d){}}var h=b.prototype;h.isSupported=function(){return this.strategy.isSupported()};h.connect= +function(b,h){var g=this.encrypted,i=a(g),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}),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(g),j.length>0?(m=Pusher.Util.now(),l=j.pop().connect(b,p)):h(a);else{var k=i.transport.name,q=Pusher.Util.now()-m,o=Pusher.Util.getLocalStorage();if(o)try{o[c(g)]= +JSON.stringify({timestamp:Pusher.Util.now(),transport:k,latency:q})}catch(r){}h(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,f=new Pusher.Timer(this.options.delay,function(){e=c.connect(a,b)});return{abort:function(){f.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,f){f&&c.abort();b(a,f)});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,f=0,g=this.timeout,i=null,j=function(k,l){l?b(null,l):(f+=1,c.loop&&(f%=e.length),f0&&(f=new Pusher.Timer(c.timeout,function(){g.abort();e(!0)}));g=a.connect(b,function(a,b){if(!a||!f||!f.isRunning()||c.failFast)f&&f.ensureAborted(),e(a,b)});return{abort:function(){f&&f.ensureAborted();g.abort()},forceMinPriority:function(a){g.forceMinPriority(a)}}}; Pusher.SequentialStrategy=b}).call(this); (function(){function b(a,b,c,f){this.name=a;this.priority=b;this.transport=c;this.options=f||{}}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()};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 h=d(a[0],b),e=c(a.slice(1),h[1]);return[[h[0]].concat(e[0]),e[1]]}function a(a,b){if(typeof a[0]==="string"&&a[0].charAt(0)===":"){var h=b[a[0].slice(1)];if(a.length>1){if(typeof h!=="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 h.apply(this,e)}else return[h, 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 h={ws:Pusher.WSTransport,flash:Pusher.FlashTransport,sockjs:Pusher.SockJSTransport},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(){}}}},f={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,f,n){var m=h[c];if(!m)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,n?n.getAssistant(m): -m,Pusher.Util.extend({key:a.key,encrypted:a.encrypted,timeline:a.timeline,ignoreNullOrigin:a.ignoreNullOrigin},f)):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})}),first_connected:b(function(a, +return{abort:function(){c.ensureAborted()},forceMinPriority:function(){}}}},f={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,f,l){var n=h[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},f)):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})}),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({},f,b);return d(a,c)[1].strategy}}}).call(this); -(function(){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")return{action:"connected",id:b.data.socket_id};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}};Pusher.Protocol=Protocol}).call(this); -(function(){function b(a,b){Pusher.EventsDispatcher.call(this);this.id=a;this.transport=b;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(){a.emit("ping_request")},e=function(b){a.emit("error",{type:"WebSocketError",error:b})},f=function(g){a.transport.unbind("closed", -f);a.transport.unbind("error",e);a.transport.unbind("ping_request",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("ping_request",c);a.transport.bind("error",e);a.transport.bind("closed",f)};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)}):(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(){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")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(f){a.transport.unbind("closed", +e);a.transport.unbind("error",c);a.transport.unbind("message",b);f&&f.code&&a.handleCloseEvent(f);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"})});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.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.resetActivityCheck=function(){this.stopActivityCheck();if(!this.connection.supportsPing()){var a=this;a.activityTimer=new Pusher.Timer(a.options.activityTimeout, -function(){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.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",{})},ping_request:function(){a.send_event("pusher:ping",{})},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.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); +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.resetActivityCheck=function(){this.stopActivityCheck();if(!this.connection.supportsPing()){var a=this;a.activityTimer=new Pusher.Timer(a.activityTimeout, +function(){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.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); diff --git a/spec/javascripts/integration/hosts_and_ports_spec.js b/spec/javascripts/integration/hosts_and_ports_spec.js index d5c25b6c0..3f2997f72 100644 --- a/spec/javascripts/integration/hosts_and_ports_spec.js +++ b/spec/javascripts/integration/hosts_and_ports_spec.js @@ -34,7 +34,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.WebSocket).toHaveBeenCalledWith( - "ws://ws.pusherapp.com:80/app/foobar?protocol=6&client=js&version=&flash=false" + "ws://ws.pusherapp.com:80/app/foobar?protocol=7&client=js&version=&flash=false" ); }); @@ -43,7 +43,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.WebSocket).toHaveBeenCalledWith( - "wss://ws.pusherapp.com:443/app/foobar?protocol=6&client=js&version=&flash=false" + "wss://ws.pusherapp.com:443/app/foobar?protocol=7&client=js&version=&flash=false" ); }); @@ -52,7 +52,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.WebSocket).toHaveBeenCalledWith( - "ws://example.com:1999/app/foobar?protocol=6&client=js&version=&flash=false" + "ws://example.com:1999/app/foobar?protocol=7&client=js&version=&flash=false" ); }); @@ -61,7 +61,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.WebSocket).toHaveBeenCalledWith( - "wss://example.org:4444/app/foobar?protocol=6&client=js&version=&flash=false" + "wss://example.org:4444/app/foobar?protocol=7&client=js&version=&flash=false" ); }); }); @@ -93,7 +93,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.FlashWebSocket).toHaveBeenCalledWith( - "ws://ws.pusherapp.com:80/app/foobar?protocol=6&client=js&version=&flash=true" + "ws://ws.pusherapp.com:80/app/foobar?protocol=7&client=js&version=&flash=true" ); }); @@ -102,7 +102,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.FlashWebSocket).toHaveBeenCalledWith( - "wss://ws.pusherapp.com:443/app/foobar?protocol=6&client=js&version=&flash=true" + "wss://ws.pusherapp.com:443/app/foobar?protocol=7&client=js&version=&flash=true" ); }); @@ -111,7 +111,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.FlashWebSocket).toHaveBeenCalledWith( - "ws://example.com:1999/app/foobar?protocol=6&client=js&version=&flash=true" + "ws://example.com:1999/app/foobar?protocol=7&client=js&version=&flash=true" ); }); @@ -120,7 +120,7 @@ describeIntegration("Host/Port Configuration", function() { pusher.connect(); expect(window.FlashWebSocket).toHaveBeenCalledWith( - "wss://example.org:4444/app/foobar?protocol=6&client=js&version=&flash=true" + "wss://example.org:4444/app/foobar?protocol=7&client=js&version=&flash=true" ); }); }); diff --git a/spec/javascripts/integration/timeout_configuration_spec.js b/spec/javascripts/integration/timeout_configuration_spec.js index 3337b9200..39d97dc8b 100644 --- a/spec/javascripts/integration/timeout_configuration_spec.js +++ b/spec/javascripts/integration/timeout_configuration_spec.js @@ -49,7 +49,7 @@ describeIntegration("Timeout Configuration", function() { expect(onUnavailable).toHaveBeenCalled(); }); - it("should obey default activity and pong timeouts", function() { + it("should obey the server's activity timeout and the default pong timeout", function() { pusher = new Pusher("foobar"); pusher.connect(); @@ -63,13 +63,14 @@ describeIntegration("Timeout Configuration", function() { data: JSON.stringify({ event: "pusher:connection_established", data: { - socket_id: "123.456" + socket_id: "123.456", + activity_timeout: 12 } }) }); expect(pusher.connection.state).toEqual("connected"); - jasmine.Clock.tick(Pusher.activity_timeout - 1); + jasmine.Clock.tick(12000 - 1); expect(firstTransport.send).not.toHaveBeenCalled(); jasmine.Clock.tick(1); expect(firstTransport.send).toHaveBeenCalled(); @@ -80,9 +81,9 @@ describeIntegration("Timeout Configuration", function() { expect(firstTransport.close).toHaveBeenCalled(); }); - it("should obey activity and pong timeouts passed as options", function() { + it("should obey the activity timeout from the handshake if it's lower than one specified in options", function() { pusher = new Pusher("foobar", { - activity_timeout: 11111, + activity_timeout: 16000, pong_timeout: 2222 }); pusher.connect(); @@ -97,17 +98,74 @@ describeIntegration("Timeout Configuration", function() { data: JSON.stringify({ event: "pusher:connection_established", data: { - socket_id: "123.456" + socket_id: "123.456", + activity_timeout: 15 } }) }); expect(pusher.connection.state).toEqual("connected"); - jasmine.Clock.tick(11110); + jasmine.Clock.tick(15000 - 1); expect(firstTransport.send).not.toHaveBeenCalled(); jasmine.Clock.tick(1); expect(firstTransport.send).toHaveBeenCalled(); + }); + + it("should obey the activity timeout specified in options if it's lower than one from the handshake", function() { + pusher = new Pusher("foobar", { + activity_timeout: 15555, + pong_timeout: 2222 + }); + pusher.connect(); + + var firstTransport = transport; + + firstTransport.state = "initialized"; + firstTransport.emit("initialized"); + firstTransport.state = "open"; + firstTransport.emit("open"); + firstTransport.emit("message", { + data: JSON.stringify({ + event: "pusher:connection_established", + data: { + socket_id: "123.456", + activity_timeout: 17 + } + }) + }); + + expect(pusher.connection.state).toEqual("connected"); + jasmine.Clock.tick(15555 - 1); + expect(firstTransport.send).not.toHaveBeenCalled(); + jasmine.Clock.tick(1); + expect(firstTransport.send).toHaveBeenCalled(); + }); + + it("should obey the pong timeout passed in options", function() { + pusher = new Pusher("foobar", { + pong_timeout: 2222 + }); + pusher.connect(); + + var firstTransport = transport; + + firstTransport.state = "initialized"; + firstTransport.emit("initialized"); + firstTransport.state = "open"; + firstTransport.emit("open"); + firstTransport.emit("message", { + data: JSON.stringify({ + event: "pusher:connection_established", + data: { + socket_id: "123.456", + activity_timeout: 120 + } + }) + }); + // first, send the ping + jasmine.Clock.tick(120000); + // wait for the pong timeout jasmine.Clock.tick(2221); expect(firstTransport.close).not.toHaveBeenCalled(); jasmine.Clock.tick(1); diff --git a/spec/javascripts/unit/connection/connection_manager_spec.js b/spec/javascripts/unit/connection/connection_manager_spec.js index a39dc0fce..87945020d 100644 --- a/spec/javascripts/unit/connection/connection_manager_spec.js +++ b/spec/javascripts/unit/connection/connection_manager_spec.js @@ -273,7 +273,11 @@ describe("ConnectionManager", function() { manager.connect(); connection.id = "123.456"; - handshake = { action: "connected", connection: connection }; + handshake = { + action: "connected", + connection: connection, + activityTimeout: 999999 + }; strategy._callback(null, handshake); }); @@ -438,14 +442,6 @@ describe("ConnectionManager", function() { .toHaveBeenCalledWith("pusher:pong", {}, undefined); }); }); - - describe("on ping request", function() { - it("should send a pusher:ping event", function() { - connection.emit("ping_request"); - expect(connection.send_event) - .toHaveBeenCalledWith("pusher:ping", {}, undefined); - }); - }); }); describe("on online event", function() { @@ -498,4 +494,68 @@ describe("ConnectionManager", function() { expect(onFailed).toHaveBeenCalled(); }); }); + + describe("with adjusted activity timeouts", function() { + var handshake; + var onConnected; + + beforeEach(function() { + manager.connect(); + connection.id = "123.456"; + }); + + it("should use the activity timeout value from the connection, if it's the lowest", function() { + connection.activityTimeout = 2666; + handshake = { + action: "connected", + connection: connection, + activityTimeout: 2667 + }; + strategy._callback(null, handshake); + + jasmine.Clock.tick(2665); + expect(connection.send_event).not.toHaveBeenCalled(); + + jasmine.Clock.tick(1); + expect(connection.send_event).toHaveBeenCalledWith( + "pusher:ping", {}, undefined + ); + }); + + it("should use the handshake activity timeout value, if it's the lowest", function() { + connection.activityTimeout = 3455; + handshake = { + action: "connected", + connection: connection, + activityTimeout: 3400 + }; + strategy._callback(null, handshake); + + jasmine.Clock.tick(3399); + expect(connection.send_event).not.toHaveBeenCalled(); + + jasmine.Clock.tick(1); + expect(connection.send_event).toHaveBeenCalledWith( + "pusher:ping", {}, undefined + ); + }); + + it("should use the default activity timeout value, if it's the lowest", function() { + connection.activityTimeout = 5555; + handshake = { + action: "connected", + connection: connection, + activityTimeout: 3500 + }; + strategy._callback(null, handshake); + + jasmine.Clock.tick(3455); + expect(connection.send_event).not.toHaveBeenCalled(); + + jasmine.Clock.tick(1); + expect(connection.send_event).toHaveBeenCalledWith( + "pusher:ping", {}, undefined + ); + }); + }); }); diff --git a/spec/javascripts/unit/connection/connection_spec.js b/spec/javascripts/unit/connection/connection_spec.js index 093901594..07c790f87 100644 --- a/spec/javascripts/unit/connection/connection_spec.js +++ b/spec/javascripts/unit/connection/connection_spec.js @@ -7,6 +7,21 @@ describe("Connection", function() { connection = new Pusher.Connection("111.22", transport); }); + describe("#activityTimeout", function() { + it("should be set to undefined if the transport doesn't have a activityTimeout value", function() { + var transport = Pusher.Mocks.getTransport(); + var connection = new Pusher.Connection("111.22", transport); + expect(connection.activityTimeout).toBe(undefined); + }); + + it("should be set to the transport's activityTimeout value", function() { + var transport = Pusher.Mocks.getTransport(); + transport.activityTimeout = 123123; + var connection = new Pusher.Connection("111.22", transport); + expect(connection.activityTimeout).toEqual(123123); + }); + }); + describe("#supportsPing", function() { it("should return true if transport supports ping", function() { transport.supportsPing.andReturn(true); @@ -61,17 +76,6 @@ describe("Connection", function() { }); }); - describe("after receiving 'ping_request' event", function() { - it("should emit 'ping_request' too", function() { - var onPingRequest = jasmine.createSpy("onPingRequest"); - connection.bind("ping_request", onPingRequest); - - transport.emit("ping_request"); - - expect(onPingRequest).toHaveBeenCalled(); - }); - }); - describe("after receiving a message", function() { it("should emit generic messages", function() { var onMessage = jasmine.createSpy("onMessage"); diff --git a/spec/javascripts/unit/connection/protocol_spec.js b/spec/javascripts/unit/connection/protocol_spec.js index a3b945b67..60f78e68f 100644 --- a/spec/javascripts/unit/connection/protocol_spec.js +++ b/spec/javascripts/unit/connection/protocol_spec.js @@ -82,14 +82,16 @@ describe("Protocol", function() { data: JSON.stringify({ event: "pusher:connection_established", data: { - socket_id: "123.456" + socket_id: "123.456", + activity_timeout: 30 } }) }; expect(Pusher.Protocol.processHandshake(message)).toEqual({ action: "connected", - id: "123.456" + id: "123.456", + activityTimeout: 30000 }); }); diff --git a/spec/javascripts/unit/strategies/cached_strategy_spec.js b/spec/javascripts/unit/strategies/cached_strategy_spec.js index 07094faab..0bb377f23 100644 --- a/spec/javascripts/unit/strategies/cached_strategy_spec.js +++ b/spec/javascripts/unit/strategies/cached_strategy_spec.js @@ -1,37 +1,36 @@ describe("CachedStrategy", function() { - var substrategy, transports, timeline; - var strategy; - var callback; - beforeEach(function() { - substrategy = Pusher.Mocks.getStrategy(true); - transports = { - test: Pusher.Mocks.getStrategy(true) - }; - timeline = Pusher.Mocks.getTimeline(); - - strategy = new Pusher.CachedStrategy(substrategy, transports, { - timeline: timeline - }); - callback = jasmine.createSpy("callback"); - jasmine.Clock.useMock(); }); describe("after calling isSupported", function() { it("should return true when the substrategy is supported", function() { - substrategy = Pusher.Mocks.getStrategy(true); - strategy = new Pusher.CachedStrategy(substrategy, {}, {}); + var substrategy = Pusher.Mocks.getStrategy(true); + var strategy = new Pusher.CachedStrategy(substrategy, {}, {}); expect(strategy.isSupported()).toBe(true); }); it("should return false when the substrategy is not supported", function() { - substrategy = Pusher.Mocks.getStrategy(false); - strategy = new Pusher.CachedStrategy(substrategy, {}, {}); + var substrategy = Pusher.Mocks.getStrategy(false); + var strategy = new Pusher.CachedStrategy(substrategy, {}, {}); expect(strategy.isSupported()).toBe(false); }); }); + describe("on browsers not supporting localStorage", function() { + beforeEach(function() { + spyOn(Pusher.Util, "getLocalStorage").andReturn(undefined); + }); + + it("should try the substrategy immediately", function() { + var substrategy = Pusher.Mocks.getStrategy(false); + var strategy = new Pusher.CachedStrategy(substrategy, {}, {}); + var callback = jasmine.createSpy("callback"); + strategy.connect(0, callback); + expect(substrategy.connect).toHaveBeenCalled(); + }); + }); + describe("on browsers supporting localStorage", function() { var localStorage; @@ -40,278 +39,314 @@ describe("CachedStrategy", function() { spyOn(Pusher.Util, "getLocalStorage").andReturn(localStorage); }); - describe("without cached transport", function() { - it("should not report anything to timeline", function() { - strategy.connect(0, callback); - expect(timeline.info).not.toHaveBeenCalled(); - }); - - it("should try the substrategy immediately when cache is empty", function() { - strategy.connect(0, callback); - expect(substrategy.connect).toHaveBeenCalled(); - }); + function buildCachedTransportTests(encrypted) { + var ENCRYPTED_KEY = "pusherTransportEncrypted"; + var UNENCRYPTED_KEY = "pusherTransportUnencrypted"; - it("should try the substrategy immediately when cache is stale", function() { - localStorage.pusherTransport = JSON.stringify({ - timestamp: Pusher.Util.now() - 1801*1000 // default ttl is 1800s - }); + var usedKey = encrypted ? ENCRYPTED_KEY : UNENCRYPTED_KEY; + var unusedKey = encrypted ? UNENCRYPTED_KEY : ENCRYPTED_KEY; - strategy.connect(0, callback); - expect(substrategy.connect).toHaveBeenCalled(); - }); - - it("should abort the substrategy when requested", function() { - var runner = strategy.connect(0, callback); - runner.abort(); - expect(substrategy._abort).toHaveBeenCalled(); - }); + var substrategy, transports, timeline; + var strategy; + var callback; - describe("on forceMinPriority", function() { - it("should force the new priority on the substrategy", function() { - var runner = strategy.connect(0, callback); - runner.forceMinPriority(10); - expect(substrategy._forceMinPriority).toHaveBeenCalledWith(10); + beforeEach(function() { + substrategy = Pusher.Mocks.getStrategy(true); + transports = { + test: Pusher.Mocks.getStrategy(true) + }; + timeline = Pusher.Mocks.getTimeline(); + + strategy = new Pusher.CachedStrategy(substrategy, transports, { + encrypted: encrypted, + timeline: timeline }); + callback = jasmine.createSpy("callback"); }); - describe("after connecting successfully", function() { - var transport; - var startTimestamp; - + describe("without cached transport, encrypted=" + encrypted, function() { beforeEach(function() { - startTimestamp = Pusher.Util.now(); - spyOn(Pusher.Util, "now").andReturn(startTimestamp); + delete localStorage[usedKey]; + localStorage[unusedKey] = "mock"; + }); + it("should not report anything to timeline", function() { strategy.connect(0, callback); - Pusher.Util.now.andReturn(startTimestamp + 1000); - - transport = Pusher.Mocks.getTransport(true); - transport.name = "test"; - substrategy._callback(null, { transport: transport }); + expect(timeline.info).not.toHaveBeenCalled(); }); - it("should call back with the transport", function() { - expect(callback).toHaveBeenCalledWith(null, { transport: transport }); + it("should try the substrategy immediately when cache is empty", function() { + strategy.connect(0, callback); + expect(substrategy.connect).toHaveBeenCalled(); }); - it("should cache the connected transport", function() { - expect(JSON.parse(localStorage.pusherTransport)).toEqual({ - timestamp: Pusher.Util.now(), - transport: "test", - latency: 1000 + it("should try the substrategy immediately when cache is stale", function() { + localStorage[usedKey] = JSON.stringify({ + timestamp: Pusher.Util.now() - 1801*1000 // default ttl is 1800s }); - }); - }); - describe("after receiving an error", function() { - beforeEach(function() { - // set a very low timestamp to have something unused in the cache - localStorage.pusherTransport = JSON.stringify({ - timestamp: 0 - }); strategy.connect(0, callback); - substrategy._callback(1); + expect(substrategy.connect).toHaveBeenCalled(); }); - it("should call back after receiving an error from the substrategy", function() { - expect(callback).toHaveBeenCalledWith(1); + it("should abort the substrategy when requested", function() { + var runner = strategy.connect(0, callback); + runner.abort(); + expect(substrategy._abort).toHaveBeenCalled(); }); - it("should flush the transport cache", function() { - expect(localStorage.pusherTransport).toBe(undefined); + describe("on forceMinPriority", function() { + it("should force the new priority on the substrategy", function() { + var runner = strategy.connect(0, callback); + runner.forceMinPriority(10); + expect(substrategy._forceMinPriority).toHaveBeenCalledWith(10); + }); }); - }); - }); - describe("with cached transport", function() { - beforeEach(function() { - cachedStrategy = Pusher.Mocks.getStrategy(true); - localStorage.pusherTransport = JSON.stringify({ - timestamp: Pusher.Util.now(), - transport: "test", - latency: 1000 - }); - }); + describe("after connecting successfully", function() { + var transport; + var startTimestamp; - it("should try the cached strategy first", function() { - strategy.connect(0, callback); - expect(transports.test.connect).toHaveBeenCalled(); - }); + beforeEach(function() { + startTimestamp = Pusher.Util.now(); + spyOn(Pusher.Util, "now").andReturn(startTimestamp); + + strategy.connect(0, callback); + Pusher.Util.now.andReturn(startTimestamp + 1000); - it("should report to timeline when using cached strategy", function() { - strategy.connect(0, callback); - expect(timeline.info).toHaveBeenCalledWith({ - cached: true, - transport: "test" + transport = Pusher.Mocks.getTransport(true); + transport.name = "test"; + substrategy._callback(null, { transport: transport }); + }); + + it("should call back with the transport", function() { + expect(callback).toHaveBeenCalledWith(null, { + transport: transport + }); + }); + + it("should cache the connected transport", function() { + expect(JSON.parse(localStorage[usedKey])).toEqual({ + timestamp: Pusher.Util.now(), + transport: "test", + latency: 1000 + }); + expect(localStorage[unusedKey]).toEqual("mock"); + }); }); - }); - it("should abort the cached transport and not call the substrategy", function() { - var runner = strategy.connect(0, callback); - runner.abort(); - expect(transports.test._abort).toHaveBeenCalled(); - expect(substrategy.connect).not.toHaveBeenCalled(); - }); + describe("after receiving an error", function() { + beforeEach(function() { + // set a very low timestamp to have something unused in the cache + localStorage[usedKey] = JSON.stringify({ + timestamp: 0 + }); + strategy.connect(0, callback); + substrategy._callback(1); + }); - describe("on forceMinPriority", function() { - it("should force the new priority on the cached strategy", function() { - var runner = strategy.connect(0, callback); - runner.forceMinPriority(1000); - expect(transports.test._forceMinPriority).toHaveBeenCalledWith(1000); + it("should call back after receiving an error from the substrategy", function() { + expect(callback).toHaveBeenCalledWith(1); + }); + + it("should flush the appropriate transport cache", function() { + expect(localStorage.pusherTransport).toBe(undefined); + expect(localStorage[unusedKey]).toEqual("mock"); + }); }); }); - describe("after connecting successfully with cached transport", function() { - var transport; + describe("with cached transport, encrypted=" + encrypted, function() { + var t0 = Pusher.Util.now(); beforeEach(function() { - startTimestamp = Pusher.Util.now(); - spyOn(Pusher.Util, "now").andReturn(startTimestamp); + cachedStrategy = Pusher.Mocks.getStrategy(true); + localStorage[usedKey] = JSON.stringify({ + timestamp: t0, + transport: "test", + latency: 1000 + }); + localStorage[unusedKey] = "mock"; + }); + it("should try the cached strategy first", function() { strategy.connect(0, callback); - Pusher.Util.now.andReturn(startTimestamp + 2000); - - transport = Pusher.Mocks.getTransport(true); - transport.name = "test"; - transports.test._callback(null, { transport: transport }); + expect(transports.test.connect).toHaveBeenCalled(); }); - it("should call back with the transport", function() { - expect(callback).toHaveBeenCalledWith(null, { transport: transport }); + it("should report to timeline when using cached strategy", function() { + strategy.connect(0, callback); + expect(timeline.info).toHaveBeenCalledWith({ + cached: true, + transport: "test" + }); }); - it("should not try the substrategy", function() { + it("should abort the cached transport and not call the substrategy", function() { + var runner = strategy.connect(0, callback); + runner.abort(); + expect(transports.test._abort).toHaveBeenCalled(); expect(substrategy.connect).not.toHaveBeenCalled(); }); - it("should cache the connected transport", function() { - expect(JSON.parse(localStorage.pusherTransport)).toEqual({ - timestamp: Pusher.Util.now(), - transport: "test", - latency: 2000 + describe("on forceMinPriority", function() { + it("should force the new priority on the cached strategy", function() { + var runner = strategy.connect(0, callback); + runner.forceMinPriority(1000); + expect(transports.test._forceMinPriority).toHaveBeenCalledWith(1000); }); }); - }); - describe("after double the cached latency", function() { - beforeEach(function() { - startTimestamp = Pusher.Util.now(); - spyOn(Pusher.Util, "now").andReturn(startTimestamp); - - strategy.connect(0, callback); + describe("after connecting successfully with the cached transport", function() { + var transport; - Pusher.Util.now.andReturn(startTimestamp + 4001); - jasmine.Clock.tick(4001); - }); + beforeEach(function() { + startTimestamp = Pusher.Util.now(); + spyOn(Pusher.Util, "now").andReturn(startTimestamp); - it("should abort the cached strategy", function() { - expect(transports.test._abort).toHaveBeenCalled(); - }); + strategy.connect(0, callback); + Pusher.Util.now.andReturn(startTimestamp + 2000); - it("should fall back to the substrategy", function() { - expect(substrategy.connect).toHaveBeenCalled(); - }); + transport = Pusher.Mocks.getTransport(true); + transport.name = "test"; + transport.options = { encrypted: false }; + transports.test._callback(null, { transport: transport }); + }); - it("should flush the transport cache", function() { - expect(localStorage.pusherTransport).toBe(undefined); - }); - }); + it("should call back with the transport", function() { + expect(callback).toHaveBeenCalledWith(null, { transport: transport }); + }); - describe("after failing to connect with the cached transport", function() { - var runner; + it("should not try the substrategy", function() { + expect(substrategy.connect).not.toHaveBeenCalled(); + }); - beforeEach(function() { - startTimestamp = Pusher.Util.now(); - spyOn(Pusher.Util, "now").andReturn(startTimestamp); - - runner = strategy.connect(0, callback); - runner.forceMinPriority(666); - Pusher.Util.now.andReturn(startTimestamp + 2000); - transports.test._callback("error"); - Pusher.Util.now.andReturn(startTimestamp + 2500); + it("should cache the connected transport", function() { + expect(JSON.parse(localStorage[usedKey])).toEqual({ + timestamp: Pusher.Util.now(), + transport: "test", + latency: 2000 + }); + expect(localStorage[unusedKey]).toEqual("mock"); + }); }); - it("should fall back to the substrategy", function() { - expect(substrategy.connect).toHaveBeenCalled(); - }); + describe("after double the cached latency", function() { + beforeEach(function() { + startTimestamp = Pusher.Util.now(); + spyOn(Pusher.Util, "now").andReturn(startTimestamp); - it("should flush the transport cache", function() { - expect(localStorage.pusherTransport).toBe(undefined); - }); + strategy.connect(0, callback); - it("should abort the substrategy when requested", function() { - runner.abort(); - expect(cachedStrategy._abort).not.toHaveBeenCalled(); - expect(substrategy._abort).toHaveBeenCalled(); - }); + Pusher.Util.now.andReturn(startTimestamp + 4001); + jasmine.Clock.tick(4001); + }); - describe("on forceMinPriority", function() { - it("should force the previously set priority on the substrategy", function() { - // priority is forced in beforeEach - expect(substrategy.connect) - .toHaveBeenCalledWith(666, jasmine.any(Function)); + it("should abort the cached strategy", function() { + expect(transports.test._abort).toHaveBeenCalled(); }); - it("should force the new priority on the substrategy", function() { - runner.forceMinPriority(3); - expect(substrategy._forceMinPriority).toHaveBeenCalledWith(3); + it("should fall back to the substrategy", function() { + expect(substrategy.connect).toHaveBeenCalled(); + }); + + it("should flush the appropriate transport cache", function() { + expect(localStorage[usedKey]).toBe(undefined); + expect(localStorage[unusedKey]).toEqual("mock"); }); }); - describe("and connecting successfully using the substrategy", function() { - var transport; + describe("after failing to connect with the cached transport", function() { + var runner; beforeEach(function() { - connection = { - name: "test", - options: { encrypted: true, "hostEncrypted": "example.net" } - }; + startTimestamp = Pusher.Util.now(); + spyOn(Pusher.Util, "now").andReturn(startTimestamp); + + runner = strategy.connect(0, callback); + runner.forceMinPriority(666); + Pusher.Util.now.andReturn(startTimestamp + 2000); + transports.test._callback("error"); + Pusher.Util.now.andReturn(startTimestamp + 2500); + }); - transport = Pusher.Mocks.getTransport(true); - transport.name = "test"; - substrategy._callback(null, { transport: transport }); + it("should fall back to the substrategy", function() { + expect(substrategy.connect).toHaveBeenCalled(); }); - it("should call back with the connection", function() { - expect(callback).toHaveBeenCalledWith(null, { - transport: transport - }); + it("should flush the appropriate transport cache", function() { + expect(localStorage[usedKey]).toBe(undefined); + expect(localStorage[unusedKey]).toEqual("mock"); }); - it("should cache the connected transport", function() { - expect(JSON.parse(localStorage.pusherTransport)).toEqual({ - timestamp: Pusher.Util.now(), - transport: "test", - latency: 500 - }); + it("should abort the substrategy when requested", function() { + runner.abort(); + expect(cachedStrategy._abort).not.toHaveBeenCalled(); + expect(substrategy._abort).toHaveBeenCalled(); }); - }); - describe("and failing to connect using the substrategy", function() { - beforeEach(function() { - substrategy._callback("error again"); + describe("on forceMinPriority", function() { + it("should force the previously set priority on the substrategy", function() { + // priority is forced in beforeEach + expect(substrategy.connect) + .toHaveBeenCalledWith(666, jasmine.any(Function)); + }); + + it("should force the new priority on the substrategy", function() { + runner.forceMinPriority(3); + expect(substrategy._forceMinPriority).toHaveBeenCalledWith(3); + }); }); - it("should call back with the error", function() { - expect(callback).toHaveBeenCalledWith("error again"); + describe("and connecting successfully using the substrategy", function() { + var transport; + + beforeEach(function() { + connection = { + name: "test", + options: { encrypted: true, "hostEncrypted": "example.net" } + }; + + transport = Pusher.Mocks.getTransport(true); + transport.name = "test"; + substrategy._callback(null, { transport: transport }); + }); + + it("should call back with the connection", function() { + expect(callback).toHaveBeenCalledWith(null, { + transport: transport + }); + }); + + it("should cache the connected transport", function() { + expect(JSON.parse(localStorage[usedKey])).toEqual({ + timestamp: Pusher.Util.now(), + transport: "test", + latency: 500 + }); + expect(localStorage[unusedKey]).toEqual("mock"); + }); }); - it("should flush the transport cache", function() { - expect(localStorage.pusherTransport).toBe(undefined); + describe("and failing to connect using the substrategy", function() { + beforeEach(function() { + substrategy._callback("error again"); + }); + + it("should call back with the error", function() { + expect(callback).toHaveBeenCalledWith("error again"); + }); + + it("should flush the appropriate transport cache", function() { + expect(localStorage[usedKey]).toBe(undefined); + expect(localStorage[unusedKey]).toEqual("mock"); + }); }); }); }); - }); - }); - - describe("on browsers not supporting localStorage", function() { - beforeEach(function() { - spyOn(Pusher.Util, "getLocalStorage").andReturn(undefined); - }); + } - it("should try the substrategy immediately", function() { - strategy.connect(0, callback); - expect(substrategy.connect).toHaveBeenCalled(); - }); + buildCachedTransportTests(false); + buildCachedTransportTests(true); }); }); diff --git a/spec/javascripts/unit/timeline/timeline_spec.js b/spec/javascripts/unit/timeline/timeline_spec.js index 2cab2bfa0..379e37a62 100644 --- a/spec/javascripts/unit/timeline/timeline_spec.js +++ b/spec/javascripts/unit/timeline/timeline_spec.js @@ -103,41 +103,6 @@ describe("Timeline", function() { expect(timeline.isEmpty()).toBe(true); }); - it("should not send extra info in second request", function() { - var sendCallback = null; - sendJSONP.andCallFake(function(data, callback) { - sendCallback = callback; - }); - var timeline = new Pusher.Timeline("foobar", 666, { - features: ["x", "y", "z"], - version: "6.6.6" - }); - - // first call - expect(timeline.send(sendJSONP, onSend)).toBe(true); - expect(sendJSONP).toHaveBeenCalledWith( - { bundle: 1, - key: "foobar", - session: 666, - features: ["x", "y", "z"], - lib: "js", - version: "6.6.6", - timeline: [] - }, - jasmine.any(Function) - ); - sendCallback(null); - // second call - expect(timeline.send(sendJSONP, onSend)).toBe(true); - expect(sendJSONP).toHaveBeenCalledWith( - { bundle: 2, - session: 666, - timeline: [] - }, - jasmine.any(Function) - ); - }); - it("should respect the size limit", function() { spyOn(Pusher.Util, "now").andReturn(123); diff --git a/spec/javascripts/unit/transports/abstract_transport_spec.js b/spec/javascripts/unit/transports/abstract_transport_spec.js index c60b76234..c251cfe6a 100644 --- a/spec/javascripts/unit/transports/abstract_transport_spec.js +++ b/spec/javascripts/unit/transports/abstract_transport_spec.js @@ -24,6 +24,23 @@ describe("AbstractTransport", function() { spyOn(this.transport, "createSocket").andReturn(this.socket); }); + describe("#activityTimeout", function() { + it("should be set to the value passed via options", function() { + var transport = getTransport("xxx", { + timeline: this.timeline, + activityTimeout: 654321 + }); + expect(transport.activityTimeout).toEqual(654321); + }); + + it("should be set to undefined if not passed via options", function() { + var transport = getTransport("xxx", { + timeline: this.timeline + }); + expect(transport.activityTimeout).toBe(undefined); + }); + }); + describe("#initialize", function() { it("should emit 'initialized' immediately", function() { var onInitialized = jasmine.createSpy("onInitialized"); @@ -75,7 +92,7 @@ describe("AbstractTransport", function() { expect(this.transport.createSocket) .toHaveBeenCalledWith( "ws://example.com:12345/app/foo" + - "?protocol=6&client=js&version=" + "?protocol=7&client=js&version=" ); }); @@ -91,7 +108,7 @@ describe("AbstractTransport", function() { expect(transport.createSocket) .toHaveBeenCalledWith( "wss://example.com:54321/app/bar" + - "?protocol=6&client=js&version=" + "?protocol=7&client=js&version=" ); }); @@ -135,7 +152,7 @@ describe("AbstractTransport", function() { expect(this.timeline.debug).toHaveBeenCalledWith({ cid: 667, method: "connect", - url: "ws://example.com:12345/app/foo?protocol=6&client=js&version=" + url: "ws://example.com:12345/app/foo?protocol=7&client=js&version=" }); }); }); @@ -217,20 +234,6 @@ describe("AbstractTransport", function() { }); }); - describe("#requestPing", function() { - it("should emit 'ping_request'", function() { - var onPingRequest = jasmine.createSpy("onPingRequest"); - this.transport.bind("ping_request", onPingRequest); - this.transport.initialize(); - this.transport.connect(); - this.socket.onopen(); - - this.transport.requestPing(); - - expect(onPingRequest).toHaveBeenCalled(); - }); - }); - describe("after receiving a message", function() { beforeEach(function() { this.transport.initialize(); diff --git a/spec/javascripts/unit/transports/assistant_to_the_transport_manager_spec.js b/spec/javascripts/unit/transports/assistant_to_the_transport_manager_spec.js index 94e0cbf8a..a985da88c 100644 --- a/spec/javascripts/unit/transports/assistant_to_the_transport_manager_spec.js +++ b/spec/javascripts/unit/transports/assistant_to_the_transport_manager_spec.js @@ -113,25 +113,11 @@ describe("AssistantToTheTransportManager", function() { expect(transportManager.reportDeath.calls.length).toEqual(1); }); - it("should send activity checks on the next connection every lifetime/2 ms", function() { + it("should set the activity timeout on the next connection to lifetime/2 ms", function() { var connection = assistant.createConnection("x", 1, "a", {}); - connection.emit("open"); - - expect(connection.requestPing).not.toHaveBeenCalled(); - jasmine.Clock.tick(94999); - expect(connection.requestPing).not.toHaveBeenCalled(); - jasmine.Clock.tick(1); - expect(connection.requestPing.calls.length).toEqual(1); - jasmine.Clock.tick(95000); - expect(connection.requestPing.calls.length).toEqual(2); - }); - - it("should stop sending activity checks on closed connections", function() { - var connection = assistant.createConnection("x", 1, "a", {}); - connection.emit("open"); - connection.emit("closed", { wasClean: true }); - jasmine.Clock.tick(100000); - expect(connection.requestPing).not.toHaveBeenCalled(); + expect(transportClass.createConnection).toHaveBeenCalledWith( + "x", 1, "a", { activityTimeout: 95000 } + ); }); }); @@ -156,11 +142,11 @@ describe("AssistantToTheTransportManager", function() { expect(transportManager.reportDeath).not.toHaveBeenCalled(); }); - it("should not send activity checks on next connection", function() { + it("should not set the activity timeout on the next connection", function() { var connection = assistant.createConnection("x", 1, "a", {}); - connection.emit("open"); - jasmine.Clock.tick(1000000); - expect(connection.requestPing).not.toHaveBeenCalled(); + expect(transportClass.createConnection).toHaveBeenCalledWith( + "x", 1, "a", {} + ); }); }); @@ -185,17 +171,11 @@ describe("AssistantToTheTransportManager", function() { expect(transportManager.reportDeath.calls.length).toEqual(1); }); - it("should send activity checks on the next connection every minPingDelay ms", function() { + it("should set the activity timeout on the next connection to minPingDelay ms", function() { var connection = assistant.createConnection("x", 1, "a", {}); - connection.emit("open"); - - expect(connection.requestPing).not.toHaveBeenCalled(); - jasmine.Clock.tick(19999); - expect(connection.requestPing).not.toHaveBeenCalled(); - jasmine.Clock.tick(1); - expect(connection.requestPing.calls.length).toEqual(1); - jasmine.Clock.tick(20000); - expect(connection.requestPing.calls.length).toEqual(2); + expect(transportClass.createConnection).toHaveBeenCalledWith( + "x", 1, "a", { activityTimeout: 20000 } + ); }); }); @@ -214,11 +194,11 @@ describe("AssistantToTheTransportManager", function() { expect(transportManager.reportDeath).not.toHaveBeenCalled(); }); - it("should not send activity checks on next connection", function() { + it("should not set the activity timeout on the next connection", function() { var connection = assistant.createConnection("x", 1, "a", {}); - connection.emit("open"); - jasmine.Clock.tick(1000000); - expect(connection.requestPing).not.toHaveBeenCalled(); + expect(transportClass.createConnection).toHaveBeenCalledWith( + "x", 1, "a", {} + ); }); }); }); diff --git a/spec/javascripts/unit/transports/flash_transport_spec.js b/spec/javascripts/unit/transports/flash_transport_spec.js index e45b7e674..13b1bd4dd 100644 --- a/spec/javascripts/unit/transports/flash_transport_spec.js +++ b/spec/javascripts/unit/transports/flash_transport_spec.js @@ -232,7 +232,7 @@ describe("FlashTransport", function() { expect(window.FlashWebSocket) .toHaveBeenCalledWith( "ws://example.com:12345/app/foo" + - "?protocol=6&client=js&version=&flash=true" + "?protocol=7&client=js&version=&flash=true" ); }); }); diff --git a/spec/javascripts/unit/transports/sockjs_transport_spec.js b/spec/javascripts/unit/transports/sockjs_transport_spec.js index f691346ff..01cf170b2 100644 --- a/spec/javascripts/unit/transports/sockjs_transport_spec.js +++ b/spec/javascripts/unit/transports/sockjs_transport_spec.js @@ -169,7 +169,7 @@ describe("SockJSTransport", function() { expect(openCallback).toHaveBeenCalled(); expect(this.socket.send).toHaveBeenCalledWith(JSON.stringify({ - path: "/app/foo?protocol=6&client=js&version=" + path: "/app/foo?protocol=7&client=js&version=" })); }); }); diff --git a/spec/javascripts/unit/transports/ws_transport_spec.js b/spec/javascripts/unit/transports/ws_transport_spec.js index ae3f82040..4610061b5 100644 --- a/spec/javascripts/unit/transports/ws_transport_spec.js +++ b/spec/javascripts/unit/transports/ws_transport_spec.js @@ -67,7 +67,7 @@ describe("WSTransport", function() { expect(window.WebSocket) .toHaveBeenCalledWith( "ws://example.com:12345/app/foo" + - "?protocol=6&client=js&version=&flash=false" + "?protocol=7&client=js&version=&flash=false" ); }); @@ -79,7 +79,7 @@ describe("WSTransport", function() { expect(window.MozWebSocket) .toHaveBeenCalledWith( "ws://example.com:12345/app/foo" + - "?protocol=6&client=js&version=&flash=false" + "?protocol=7&client=js&version=&flash=false" ); }); }); diff --git a/src/connection/connection.js b/src/connection/connection.js index b058d42b4..6c3bd46bc 100644 --- a/src/connection/connection.js +++ b/src/connection/connection.js @@ -20,6 +20,7 @@ this.id = id; this.transport = transport; + this.activityTimeout = transport.activityTimeout; this.bindListeners(); } var prototype = Connection.prototype; @@ -95,9 +96,6 @@ self.emit('message', message); } }; - var onPingRequest = function() { - self.emit("ping_request"); - }; var onError = function(error) { self.emit("error", { type: "WebSocketError", error: error }); }; @@ -115,12 +113,10 @@ var unbindListeners = function() { self.transport.unbind("closed", onClosed); self.transport.unbind("error", onError); - self.transport.unbind("ping_request", onPingRequest); self.transport.unbind("message", onMessage); }; self.transport.bind("message", onMessage); - self.transport.bind("ping_request", onPingRequest); self.transport.bind("error", onError); self.transport.bind("closed", onClosed); }; diff --git a/src/connection/connection_manager.js b/src/connection/connection_manager.js index 236ddc92d..a7b0fc4db 100644 --- a/src/connection/connection_manager.js +++ b/src/connection/connection_manager.js @@ -205,7 +205,7 @@ if (!this.connection.supportsPing()) { var self = this; self.activityTimer = new Pusher.Timer( - self.options.activityTimeout, + self.activityTimeout, function() { self.send_event('pusher:ping', {}); // wait for pong response @@ -240,9 +240,6 @@ ping: function() { self.send_event('pusher:pong', {}); }, - ping_request: function() { - self.send_event('pusher:ping', {}); - }, error: function(error) { // just emit error to user - socket will already be closed by browser self.emit("error", { type: "WebSocketError", error: error }); @@ -261,6 +258,11 @@ 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; diff --git a/src/connection/handshake.js b/src/connection/handshake.js index 51fcb44b9..3e965966c 100644 --- a/src/connection/handshake.js +++ b/src/connection/handshake.js @@ -38,7 +38,8 @@ var result = Pusher.Protocol.processHandshake(m); if (result.action === "connected") { self.finish("connected", { - connection: new Pusher.Connection(result.id, self.transport) + connection: new Pusher.Connection(result.id, self.transport), + activityTimeout: result.activityTimeout }); } else { self.finish(result.action, { error: result.error }); diff --git a/src/connection/protocol.js b/src/connection/protocol.js index 4ddb3198a..73fcafb6a 100644 --- a/src/connection/protocol.js +++ b/src/connection/protocol.js @@ -2,7 +2,7 @@ /** * Provides functions for handling Pusher protocol-specific messages. */ - Protocol = {}; + var Protocol = {}; /** * Decodes a message in a Pusher format. @@ -59,7 +59,11 @@ message = this.decodeMessage(message); if (message.event === "pusher:connection_established") { - return { action: "connected", id: message.data.socket_id }; + 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 diff --git a/src/defaults.js b/src/defaults.js index 7d8cb6786..1792c48ef 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,6 +1,6 @@ ;(function() { Pusher.VERSION = ''; - Pusher.PROTOCOL = 6; + Pusher.PROTOCOL = 7; // DEPRECATED: WS connection parameters Pusher.host = 'ws.pusherapp.com'; diff --git a/src/pusher.js b/src/pusher.js index b51d03df9..12fb35213 100644 --- a/src/pusher.js +++ b/src/pusher.js @@ -78,6 +78,7 @@ }); Pusher.instances.push(this); + this.timeline.info({ instances: Pusher.instances.length }); if (Pusher.isReady) self.connect(); } diff --git a/src/strategies/cached_strategy.js b/src/strategies/cached_strategy.js index 25aeaa3dd..b21b76070 100644 --- a/src/strategies/cached_strategy.js +++ b/src/strategies/cached_strategy.js @@ -9,6 +9,7 @@ this.strategy = strategy; this.transports = transports; this.ttl = options.ttl || 1800*1000; + this.encrypted = options.encrypted; this.timeline = options.timeline; } var prototype = CachedStrategy.prototype; @@ -18,7 +19,8 @@ }; prototype.connect = function(minPriority, callback) { - var info = fetchTransportInfo(); + var encrypted = this.encrypted; + var info = fetchTransportCache(encrypted); var strategies = [this.strategy]; if (info && info.timestamp + this.ttl >= Pusher.Util.now()) { @@ -37,7 +39,7 @@ minPriority, function cb(error, handshake) { if (error) { - flushTransportInfo(); + flushTransportCache(encrypted); if (strategies.length > 0) { startTimestamp = Pusher.Util.now(); runner = strategies.pop().connect(minPriority, cb); @@ -45,8 +47,11 @@ callback(error); } } else { - var latency = Pusher.Util.now() - startTimestamp; - storeTransportInfo(handshake.transport.name, latency); + storeTransportCache( + encrypted, + handshake.transport.name, + Pusher.Util.now() - startTimestamp + ); callback(null, handshake); } } @@ -65,26 +70,30 @@ }; }; - function fetchTransportInfo() { + function getTransportCacheKey(encrypted) { + return "pusherTransport" + (encrypted ? "Encrypted" : "Unencrypted"); + } + + function fetchTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { - var info = storage.pusherTransport; - if (info) { - return JSON.parse(info); + var serializedCache = storage[getTransportCacheKey(encrypted)]; + if (serializedCache) { + return JSON.parse(serializedCache); } } catch (e) { - flushTransportInfo(); + flushTransportCache(encrypted); } } return null; } - function storeTransportInfo(transport, latency) { + function storeTransportCache(encrypted, transport, latency) { var storage = Pusher.Util.getLocalStorage(); if (storage) { try { - storage.pusherTransport = JSON.stringify({ + storage[getTransportCacheKey(encrypted)] = JSON.stringify({ timestamp: Pusher.Util.now(), transport: transport, latency: latency @@ -95,13 +104,13 @@ } } - function flushTransportInfo() { + function flushTransportCache(encrypted) { var storage = Pusher.Util.getLocalStorage(); - if (storage && storage.pusherTransport) { + if (storage) { try { - delete storage.pusherTransport; + delete storage[getTransportCacheKey(encrypted)]; } catch (e) { - storage.pusherTransport = undefined; + // catch exceptions raised by localStorage } } } diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 65e987b0a..9be38dcea 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -47,22 +47,15 @@ prototype.send = function(sendJSONP, callback) { var self = this; - var data = { + 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 - }; - if (self.sent === 0) { - Pusher.Util.extend(data, { - key: self.key, - features: self.options.features, - lib: "js", - version: self.options.version - }, self.options.params || {}); - } - data = Pusher.Util.filterObject(data, function(v) { - return v !== undefined; - }); + }, self.options.params); self.events = []; sendJSONP(data, function(error, result) { diff --git a/src/timeline/timeline_sender.js b/src/timeline/timeline_sender.js index 5a6a890ca..91a83cf3f 100644 --- a/src/timeline/timeline_sender.js +++ b/src/timeline/timeline_sender.js @@ -15,7 +15,9 @@ var sendJSONP = function(data, callback) { var params = { - data: data, + data: Pusher.Util.filterObject(data, function(v) { + return v !== undefined; + }), url: scheme + (self.host || self.options.host) + self.options.path, receiver: Pusher.JSONP }; diff --git a/src/transports/abstract_transport.js b/src/transports/abstract_transport.js index f12b0784f..a27a6e83a 100644 --- a/src/transports/abstract_transport.js +++ b/src/transports/abstract_transport.js @@ -37,6 +37,7 @@ this.key = key; this.state = "new"; this.timeline = options.timeline; + this.activityTimeout = options.activityTimeout; this.id = this.timeline.generateUniqueID(); this.options = { @@ -150,10 +151,6 @@ } }; - prototype.requestPing = function() { - this.emit("ping_request"); - }; - /** @protected */ prototype.onOpen = function() { this.changeState("open"); diff --git a/src/transports/assistant_to_the_transport_manager.js b/src/transports/assistant_to_the_transport_manager.js index 2fbec3aa7..f553d64f4 100644 --- a/src/transports/assistant_to_the_transport_manager.js +++ b/src/transports/assistant_to_the_transport_manager.js @@ -4,39 +4,29 @@ this.transport = transport; this.minPingDelay = options.minPingDelay; this.maxPingDelay = options.maxPingDelay; - this.pingDelay = null; + this.pingDelay = undefined; } var prototype = AssistantToTheTransportManager.prototype; prototype.createConnection = function(name, priority, key, options) { - var connection = this.transport.createConnection( + var self = this; + + var options = Pusher.Util.extend({}, options, { + activityTimeout: self.pingDelay + }); + var connection = self.transport.createConnection( name, priority, key, options ); - var self = this; var openTimestamp = null; - var pingTimer = null; var onOpen = function() { connection.unbind("open", onOpen); - - openTimestamp = Pusher.Util.now(); - if (self.pingDelay) { - pingTimer = setInterval(function() { - if (pingTimer) { - connection.requestPing(); - } - }, self.pingDelay); - } - connection.bind("closed", onClosed); + openTimestamp = Pusher.Util.now(); }; var onClosed = function(closeEvent) { connection.unbind("closed", onClosed); - if (pingTimer) { - clearInterval(pingTimer); - pingTimer = null; - } if (closeEvent.code === 1002 || closeEvent.code === 1003) { // we don't want to use transports not obeying the protocol diff --git a/src/transports/flash_transport.js b/src/transports/flash_transport.js index ee1ddb8fd..3aefe7bd3 100644 --- a/src/transports/flash_transport.js +++ b/src/transports/flash_transport.js @@ -32,11 +32,15 @@ try { return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash')); } catch (e) { - return Boolean( - navigator && - navigator.mimeTypes && - navigator.mimeTypes["application/x-shockwave-flash"] !== undefined - ); + try { + return Boolean( + navigator && + navigator.mimeTypes && + navigator.mimeTypes["application/x-shockwave-flash"] !== undefined + ); + } catch(e) { + return false; + } } };