pusher-fake 0.14.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bdcb9226aaf3bc12a6ecaf05a7a8caf22bd7580f
4
- data.tar.gz: bd8a9a836ccdce64bb5173db0b6d2bc1f0c35342
3
+ metadata.gz: 4018221dfac239178547457cb5f01ff6de4fa6a5
4
+ data.tar.gz: 96ddd96fce5d6b969c965ffa197fc5b38aa51d40
5
5
  SHA512:
6
- metadata.gz: 55fce89eb072245cde7dd9a6c5ecf49eb733102c1dda0e48ee80781fa2054ba9f9471eadb531333a39dad1f1cd45d63e722d95e7cbc98650c9dc6c04421cb6c0
7
- data.tar.gz: 129cf3175bf0cee5d50710f4762f16ab079d89133fe1b66eca6874e408cba1a3309b1d40d0a1740e313e89759738bbe9c8a105780294379e53d31783c31742d9
6
+ metadata.gz: 069edb3dc3fe2a368023b154b7f24fae69a4083cfc536f6552e0471ed40255ba749f8138597ddae3e9901b60ab9121fc82c4f70bd3cb2751b3e471fa4e846d95
7
+ data.tar.gz: e99741e224800cf12a1c814a1d25f233172956490232e8020540336b0584ce930fe7cd146b4d5a169e428e30000cb07cd677e69e43dbbd2841138822c5b247f9
@@ -29,12 +29,12 @@ end
29
29
 
30
30
  Then /^I should receive JSON for (\d+) users?$/ do |count|
31
31
  @response[:users].tap do |users|
32
- expect(users).to have(count).items
32
+ expect(users.length).to eq(count.to_i)
33
33
 
34
34
  users.map do |user|
35
- ObjectSpace._id2ref(user["id"])
35
+ ObjectSpace._id2ref(user["id"].to_i)
36
36
  end.each do |object|
37
- expect(object).to be_a(PusherFake::Connection)
37
+ expect(object).to be_a(EventMachine::WebSocket::Connection)
38
38
  end
39
39
  end
40
40
  end
@@ -81,6 +81,6 @@ Then %{I should not be subscribed to the "$channel" channel} do |channel|
81
81
  channel && channel.subscribed;
82
82
  })
83
83
 
84
- expect(subscribed).to be_false
84
+ expect(subscribed).to eq(false)
85
85
  end
86
86
  end
@@ -38,7 +38,7 @@ Then /^([^ ]+) should receive a "([^"]+)" event on the "([^"]+)" channel$/ do |n
38
38
  wait do
39
39
  events = page.evaluate_script("Pusher.instance.events[#{MultiJson.dump([channel, event].join(":"))}]")
40
40
 
41
- expect(events).to have(1).item
41
+ expect(events.length).to eq(1)
42
42
  end
43
43
  end
44
44
  end
@@ -1,8 +1,8 @@
1
1
  /*!
2
- * Pusher JavaScript Library v2.1.6
3
- * http://pusherapp.com/
2
+ * Pusher JavaScript Library v2.2.1
3
+ * http://pusher.com/
4
4
  *
5
- * Copyright 2013, Pusher
5
+ * Copyright 2014, Pusher
6
6
  * Released under the MIT licence.
7
7
  */
8
8
 
@@ -25,6 +25,7 @@
25
25
  this.sessionID = Math.floor(Math.random() * 1000000000);
26
26
 
27
27
  this.timeline = new Pusher.Timeline(this.key, this.sessionID, {
28
+ cluster: this.config.cluster,
28
29
  features: Pusher.Util.getClientFeatures(),
29
30
  params: this.config.timelineParams || {},
30
31
  limit: 50,
@@ -34,14 +35,14 @@
34
35
  if (!this.config.disableStats) {
35
36
  this.timelineSender = new Pusher.TimelineSender(this.timeline, {
36
37
  host: this.config.statsHost,
37
- path: "/timeline"
38
+ path: "/timeline/v2/jsonp"
38
39
  });
39
40
  }
40
41
 
41
42
  var getStrategy = function(options) {
43
+ var config = Pusher.Util.extend({}, self.config, options);
42
44
  return Pusher.StrategyBuilder.build(
43
- Pusher.getDefaultStrategy(self.config),
44
- Pusher.Util.extend({}, self.config, options)
45
+ Pusher.getDefaultStrategy(config), config
45
46
  );
46
47
  };
47
48
 
@@ -88,7 +89,9 @@
88
89
  Pusher.instances.push(this);
89
90
  this.timeline.info({ instances: Pusher.instances.length });
90
91
 
91
- if (Pusher.isReady) self.connect();
92
+ if (Pusher.isReady) {
93
+ self.connect();
94
+ }
92
95
  }
93
96
  var prototype = Pusher.prototype;
94
97
 
@@ -195,7 +198,7 @@
195
198
  };
196
199
 
197
200
  prototype.isEncrypted = function() {
198
- if (Pusher.Util.getDocumentLocation().protocol === "https:") {
201
+ if (Pusher.Util.getDocument().location.protocol === "https:") {
199
202
  return true;
200
203
  } else {
201
204
  return Boolean(this.config.encrypted);
@@ -210,80 +213,71 @@
210
213
  }
211
214
  }
212
215
 
216
+ Pusher.HTTP = {};
217
+
213
218
  this.Pusher = Pusher;
214
219
  }).call(this);
215
220
 
216
221
  ;(function() {
217
- /** Cross-browser compatible timer abstraction.
218
- *
219
- * @param {Number} delay
220
- * @param {Function} callback
221
- */
222
- function Timer(delay, callback) {
222
+ // We need to bind clear functions this way to avoid exceptions on IE8
223
+ function clearTimeout(timer) {
224
+ window.clearTimeout(timer);
225
+ }
226
+ function clearInterval(timer) {
227
+ window.clearInterval(timer);
228
+ }
229
+
230
+ function GenericTimer(set, clear, delay, callback) {
223
231
  var self = this;
224
232
 
225
- this.timeout = setTimeout(function() {
226
- if (self.timeout !== null) {
227
- callback();
228
- self.timeout = null;
233
+ this.clear = clear;
234
+ this.timer = set(function() {
235
+ if (self.timer !== null) {
236
+ self.timer = callback(self.timer);
229
237
  }
230
238
  }, delay);
231
239
  }
232
- var prototype = Timer.prototype;
240
+ var prototype = GenericTimer.prototype;
233
241
 
234
242
  /** Returns whether the timer is still running.
235
243
  *
236
244
  * @return {Boolean}
237
245
  */
238
246
  prototype.isRunning = function() {
239
- return this.timeout !== null;
247
+ return this.timer !== null;
240
248
  };
241
249
 
242
250
  /** Aborts a timer when it's running. */
243
251
  prototype.ensureAborted = function() {
244
- if (this.timeout) {
245
- clearTimeout(this.timeout);
246
- this.timeout = null;
252
+ if (this.timer) {
253
+ // Clear function is already bound
254
+ this.clear(this.timer);
255
+ this.timer = null;
247
256
  }
248
257
  };
249
258
 
250
- Pusher.Timer = Timer;
251
- }).call(this);
252
-
253
- ;(function() {
254
- /** Cross-browser compatible periodic timer abstraction.
259
+ /** Cross-browser compatible one-off timer abstraction.
255
260
  *
256
- * @param {Number} interval
261
+ * @param {Number} delay
257
262
  * @param {Function} callback
258
263
  */
259
- function PeriodicTimer(interval, callback) {
260
- var self = this;
261
-
262
- this.interval = setInterval(function() {
263
- if (self.interval !== null) {
264
- callback();
265
- }
266
- }, interval);
267
- }
268
- var prototype = PeriodicTimer.prototype;
269
-
270
- /** Returns whether the timer is still running.
264
+ Pusher.Timer = function(delay, callback) {
265
+ return new GenericTimer(setTimeout, clearTimeout, delay, function(timer) {
266
+ callback();
267
+ return null;
268
+ });
269
+ };
270
+ /** Cross-browser compatible periodic timer abstraction.
271
271
  *
272
- * @return {Boolean}
272
+ * @param {Number} delay
273
+ * @param {Function} callback
273
274
  */
274
- prototype.isRunning = function() {
275
- return this.interval !== null;
276
- };
277
-
278
- /** Aborts a timer when it's running. */
279
- prototype.ensureAborted = function() {
280
- if (this.interval) {
281
- clearInterval(this.interval);
282
- this.interval = null;
283
- }
275
+ Pusher.PeriodicTimer = function(delay, callback) {
276
+ return new GenericTimer(setInterval, clearInterval, delay, function(timer) {
277
+ callback();
278
+ return timer;
279
+ });
284
280
  };
285
-
286
- Pusher.PeriodicTimer = PeriodicTimer;
287
281
  }).call(this);
288
282
 
289
283
  ;(function() {
@@ -417,9 +411,9 @@
417
411
  * @param {Array} array
418
412
  * @param {Function} f
419
413
  */
420
- apply: function(array, f) {
414
+ apply: function(array, f, context) {
421
415
  for (var i = 0; i < array.length; i++) {
422
- f(array[i], i, array);
416
+ f.call(context || window, array[i], i, array);
423
417
  }
424
418
  },
425
419
 
@@ -571,12 +565,16 @@
571
565
  };
572
566
  },
573
567
 
568
+ getWindow: function() {
569
+ return window;
570
+ },
571
+
574
572
  getDocument: function() {
575
573
  return document;
576
574
  },
577
575
 
578
- getDocumentLocation: function() {
579
- return Pusher.Util.getDocument().location;
576
+ getNavigator: function() {
577
+ return navigator;
580
578
  },
581
579
 
582
580
  getLocalStorage: function() {
@@ -591,15 +589,44 @@
591
589
  return Pusher.Util.keys(
592
590
  Pusher.Util.filterObject(
593
591
  { "ws": Pusher.WSTransport, "flash": Pusher.FlashTransport },
594
- function (t) { return t.isSupported(); }
592
+ function (t) { return t.isSupported({}); }
595
593
  )
596
594
  );
595
+ },
596
+
597
+ addWindowListener: function(event, listener) {
598
+ var _window = Pusher.Util.getWindow();
599
+ if (_window.addEventListener !== undefined) {
600
+ _window.addEventListener(event, listener, false);
601
+ } else {
602
+ _window.attachEvent("on" + event, listener);
603
+ }
604
+ },
605
+
606
+ removeWindowListener: function(event, listener) {
607
+ var _window = Pusher.Util.getWindow();
608
+ if (_window.addEventListener !== undefined) {
609
+ _window.removeEventListener(event, listener, false);
610
+ } else {
611
+ _window.detachEvent("on" + event, listener);
612
+ }
613
+ },
614
+
615
+ isXHRSupported: function() {
616
+ var XHR = window.XMLHttpRequest;
617
+ return Boolean(XHR) && (new XHR()).withCredentials !== undefined;
618
+ },
619
+
620
+ isXDRSupported: function(encrypted) {
621
+ var protocol = encrypted ? "https:" : "http:";
622
+ var documentProtocol = Pusher.Util.getDocument().location.protocol;
623
+ return Boolean(window.XDomainRequest) && documentProtocol === protocol;
597
624
  }
598
625
  };
599
626
  }).call(this);
600
627
 
601
628
  ;(function() {
602
- Pusher.VERSION = '2.1.6';
629
+ Pusher.VERSION = '2.2.1';
603
630
  Pusher.PROTOCOL = 7;
604
631
 
605
632
  // DEPRECATED: WS connection parameters
@@ -625,11 +652,30 @@
625
652
  Pusher.dependency_suffix = '';
626
653
 
627
654
  Pusher.getDefaultStrategy = function(config) {
655
+ var wsStrategy;
656
+ if (config.encrypted) {
657
+ wsStrategy = [
658
+ ":best_connected_ever",
659
+ ":ws_loop",
660
+ [":delayed", 2000, [":http_fallback_loop"]]
661
+ ];
662
+ } else {
663
+ wsStrategy = [
664
+ ":best_connected_ever",
665
+ ":ws_loop",
666
+ [":delayed", 2000, [":wss_loop"]],
667
+ [":delayed", 5000, [":http_fallback_loop"]]
668
+ ];
669
+ }
670
+
628
671
  return [
629
672
  [":def", "ws_options", {
630
673
  hostUnencrypted: config.wsHost + ":" + config.wsPort,
631
674
  hostEncrypted: config.wsHost + ":" + config.wssPort
632
675
  }],
676
+ [":def", "wss_options", [":extend", ":ws_options", {
677
+ encrypted: true
678
+ }]],
633
679
  [":def", "sockjs_options", {
634
680
  hostUnencrypted: config.httpHost + ":" + config.httpPort,
635
681
  hostEncrypted: config.httpHost + ":" + config.httpsPort
@@ -645,25 +691,67 @@
645
691
  minPingDelay: 10000,
646
692
  maxPingDelay: config.activity_timeout
647
693
  }]],
694
+ [":def", "streaming_manager", [":transport_manager", {
695
+ lives: 2,
696
+ minPingDelay: 10000,
697
+ maxPingDelay: config.activity_timeout
698
+ }]],
648
699
 
649
700
  [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"],
701
+ [":def_transport", "wss", "ws", 3, ":wss_options", ":ws_manager"],
650
702
  [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"],
651
703
  [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"],
704
+ [":def_transport", "xhr_streaming", "xhr_streaming", 1, ":sockjs_options", ":streaming_manager"],
705
+ [":def_transport", "xdr_streaming", "xdr_streaming", 1, ":sockjs_options", ":streaming_manager"],
706
+ [":def_transport", "xhr_polling", "xhr_polling", 1, ":sockjs_options"],
707
+ [":def_transport", "xdr_polling", "xdr_polling", 1, ":sockjs_options"],
708
+
652
709
  [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]],
710
+ [":def", "wss_loop", [":sequential", ":timeouts", ":wss"]],
653
711
  [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]],
654
712
  [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
655
713
 
714
+ [":def", "streaming_loop", [":sequential", ":timeouts",
715
+ [":if", [":is_supported", ":xhr_streaming"],
716
+ ":xhr_streaming",
717
+ ":xdr_streaming"
718
+ ]
719
+ ]],
720
+ [":def", "polling_loop", [":sequential", ":timeouts",
721
+ [":if", [":is_supported", ":xhr_polling"],
722
+ ":xhr_polling",
723
+ ":xdr_polling"
724
+ ]
725
+ ]],
726
+
727
+ [":def", "http_loop", [":if", [":is_supported", ":streaming_loop"], [
728
+ ":best_connected_ever",
729
+ ":streaming_loop",
730
+ [":delayed", 4000, [":polling_loop"]]
731
+ ], [
732
+ ":polling_loop"
733
+ ]]],
734
+
735
+ [":def", "http_fallback_loop",
736
+ [":if", [":is_supported", ":http_loop"], [
737
+ ":http_loop"
738
+ ], [
739
+ ":sockjs_loop"
740
+ ]]
741
+ ],
742
+
656
743
  [":def", "strategy",
657
744
  [":cached", 1800000,
658
745
  [":first_connected",
659
- [":if", [":is_supported", ":ws"], [
660
- ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]]
661
- ], [":if", [":is_supported", ":flash"], [
662
- ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":sockjs_loop"]]
663
- ], [
664
- ":sockjs_loop"
665
- ]
666
- ]]
746
+ [":if", [":is_supported", ":ws"],
747
+ wsStrategy,
748
+ [":if", [":is_supported", ":flash"], [
749
+ ":best_connected_ever",
750
+ ":flash_loop",
751
+ [":delayed", 2000, [":http_fallback_loop"]]
752
+ ], [
753
+ ":http_fallback_loop"
754
+ ]]]
667
755
  ]
668
756
  ]
669
757
  ]
@@ -701,22 +789,23 @@
701
789
 
702
790
  ;(function() {
703
791
  function buildExceptionClass(name) {
704
- var klass = function(message) {
792
+ var constructor = function(message) {
705
793
  Error.call(this, message);
706
794
  this.name = name;
707
795
  };
708
- Pusher.Util.extend(klass.prototype, Error.prototype);
796
+ Pusher.Util.extend(constructor.prototype, Error.prototype);
709
797
 
710
- return klass;
798
+ return constructor;
711
799
  }
712
800
 
713
801
  /** Error classes used throughout pusher-js library. */
714
802
  Pusher.Errors = {
715
803
  BadEventName: buildExceptionClass("BadEventName"),
716
- UnsupportedTransport: buildExceptionClass("UnsupportedTransport"),
717
- UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy"),
804
+ RequestTimedOut: buildExceptionClass("RequestTimedOut"),
718
805
  TransportPriorityTooLow: buildExceptionClass("TransportPriorityTooLow"),
719
- TransportClosed: buildExceptionClass("TransportClosed")
806
+ TransportClosed: buildExceptionClass("TransportClosed"),
807
+ UnsupportedTransport: buildExceptionClass("UnsupportedTransport"),
808
+ UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy")
720
809
  };
721
810
  }).call(this);
722
811
 
@@ -732,8 +821,8 @@
732
821
  }
733
822
  var prototype = EventsDispatcher.prototype;
734
823
 
735
- prototype.bind = function(eventName, callback) {
736
- this.callbacks.add(eventName, callback);
824
+ prototype.bind = function(eventName, callback, context) {
825
+ this.callbacks.add(eventName, callback, context);
737
826
  return this;
738
827
  };
739
828
 
@@ -742,7 +831,12 @@
742
831
  return this;
743
832
  };
744
833
 
745
- prototype.unbind = function(eventName, callback) {
834
+ prototype.unbind = function(eventName, callback, context) {
835
+ this.callbacks.remove(eventName, callback, context);
836
+ return this;
837
+ };
838
+
839
+ prototype.unbind_all = function(eventName, callback) {
746
840
  this.callbacks.remove(eventName, callback);
747
841
  return this;
748
842
  };
@@ -757,7 +851,7 @@
757
851
  var callbacks = this.callbacks.get(eventName);
758
852
  if (callbacks && callbacks.length > 0) {
759
853
  for (i = 0; i < callbacks.length; i++) {
760
- callbacks[i](data);
854
+ callbacks[i].fn.call(callbacks[i].context || window, data);
761
855
  }
762
856
  } else if (this.failThrough) {
763
857
  this.failThrough(eventName, data);
@@ -772,36 +866,205 @@
772
866
  this._callbacks = {};
773
867
  }
774
868
 
775
- CallbackRegistry.prototype.get = function(eventName) {
776
- return this._callbacks[this._prefix(eventName)];
869
+ CallbackRegistry.prototype.get = function(name) {
870
+ return this._callbacks[prefix(name)];
777
871
  };
778
872
 
779
- CallbackRegistry.prototype.add = function(eventName, callback) {
780
- var prefixedEventName = this._prefix(eventName);
873
+ CallbackRegistry.prototype.add = function(name, callback, context) {
874
+ var prefixedEventName = prefix(name);
781
875
  this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || [];
782
- this._callbacks[prefixedEventName].push(callback);
876
+ this._callbacks[prefixedEventName].push({
877
+ fn: callback,
878
+ context: context
879
+ });
880
+ };
881
+
882
+ CallbackRegistry.prototype.remove = function(name, callback, context) {
883
+ if (!name && !callback && !context) {
884
+ this._callbacks = {};
885
+ return;
886
+ }
887
+
888
+ var names = name ? [prefix(name)] : Pusher.Util.keys(this._callbacks);
889
+
890
+ if (callback || context) {
891
+ Pusher.Util.apply(names, function(name) {
892
+ this._callbacks[name] = Pusher.Util.filter(
893
+ this._callbacks[name] || [],
894
+ function(binding) {
895
+ return (callback && callback !== binding.fn) ||
896
+ (context && context !== binding.context);
897
+ }
898
+ );
899
+ if (this._callbacks[name].length === 0) {
900
+ delete this._callbacks[name];
901
+ }
902
+ }, this);
903
+ } else {
904
+ Pusher.Util.apply(names, function(name) {
905
+ delete this._callbacks[name];
906
+ }, this);
907
+ }
783
908
  };
784
909
 
785
- CallbackRegistry.prototype.remove = function(eventName, callback) {
786
- if(this.get(eventName)) {
787
- var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback);
788
- if (index !== -1){
789
- var callbacksCopy = this._callbacks[this._prefix(eventName)].slice(0);
790
- callbacksCopy.splice(index, 1);
791
- this._callbacks[this._prefix(eventName)] = callbacksCopy;
910
+ function prefix(name) {
911
+ return "_" + name;
912
+ }
913
+
914
+ Pusher.EventsDispatcher = EventsDispatcher;
915
+ }).call(this);
916
+
917
+ (function() {
918
+ /** Builds receivers for JSONP and Script requests.
919
+ *
920
+ * Each receiver is an object with following fields:
921
+ * - number - unique (for the factory instance), numerical id of the receiver
922
+ * - id - a string ID that can be used in DOM attributes
923
+ * - name - name of the function triggering the receiver
924
+ * - callback - callback function
925
+ *
926
+ * Receivers are triggered only once, on the first callback call.
927
+ *
928
+ * Receivers can be called by their name or by accessing factory object
929
+ * by the number key.
930
+ *
931
+ * @param {String} prefix the prefix used in ids
932
+ * @param {String} name the name of the object
933
+ */
934
+ function ScriptReceiverFactory(prefix, name) {
935
+ this.lastId = 0;
936
+ this.prefix = prefix;
937
+ this.name = name;
938
+ }
939
+ var prototype = ScriptReceiverFactory.prototype;
940
+
941
+ /** Creates a script receiver.
942
+ *
943
+ * @param {Function} callback
944
+ * @return {ScriptReceiver}
945
+ */
946
+ prototype.create = function(callback) {
947
+ this.lastId++;
948
+
949
+ var number = this.lastId;
950
+ var id = this.prefix + number;
951
+ var name = this.name + "[" + number + "]";
952
+
953
+ var called = false;
954
+ var callbackWrapper = function() {
955
+ if (!called) {
956
+ callback.apply(null, arguments);
957
+ called = true;
792
958
  }
959
+ };
960
+
961
+ this[number] = callbackWrapper;
962
+ return { number: number, id: id, name: name, callback: callbackWrapper };
963
+ };
964
+
965
+ /** Removes the script receiver from the list.
966
+ *
967
+ * @param {ScriptReceiver} receiver
968
+ */
969
+ prototype.remove = function(receiver) {
970
+ delete this[receiver.number];
971
+ };
972
+
973
+ Pusher.ScriptReceiverFactory = ScriptReceiverFactory;
974
+ Pusher.ScriptReceivers = new ScriptReceiverFactory(
975
+ "_pusher_script_", "Pusher.ScriptReceivers"
976
+ );
977
+ }).call(this);
978
+
979
+ (function() {
980
+ /** Sends a generic HTTP GET request using a script tag.
981
+ *
982
+ * By constructing URL in a specific way, it can be used for loading
983
+ * JavaScript resources or JSONP requests. It can notify about errors, but
984
+ * only in certain environments. Please take care of monitoring the state of
985
+ * the request yourself.
986
+ *
987
+ * @param {String} src
988
+ */
989
+ function ScriptRequest(src) {
990
+ this.src = src;
991
+ }
992
+ var prototype = ScriptRequest.prototype;
993
+
994
+ /** Sends the actual script request.
995
+ *
996
+ * @param {ScriptReceiver} receiver
997
+ */
998
+ prototype.send = function(receiver) {
999
+ var self = this;
1000
+ var errorString = "Error loading " + self.src;
1001
+
1002
+ self.script = document.createElement("script");
1003
+ self.script.id = receiver.id;
1004
+ self.script.src = self.src;
1005
+ self.script.type = "text/javascript";
1006
+ self.script.charset = "UTF-8";
1007
+
1008
+ if (self.script.addEventListener) {
1009
+ self.script.onerror = function() {
1010
+ receiver.callback(errorString);
1011
+ };
1012
+ self.script.onload = function() {
1013
+ receiver.callback(null);
1014
+ };
1015
+ } else {
1016
+ self.script.onreadystatechange = function() {
1017
+ if (self.script.readyState === 'loaded' ||
1018
+ self.script.readyState === 'complete') {
1019
+ receiver.callback(null);
1020
+ }
1021
+ };
1022
+ }
1023
+
1024
+ // Opera<11.6 hack for missing onerror callback
1025
+ if (self.script.async === undefined && document.attachEvent &&
1026
+ /opera/i.test(navigator.userAgent)) {
1027
+ self.errorScript = document.createElement("script");
1028
+ self.errorScript.id = receiver.id + "_error";
1029
+ self.errorScript.text = receiver.name + "('" + errorString + "');";
1030
+ self.script.async = self.errorScript.async = false;
1031
+ } else {
1032
+ self.script.async = true;
1033
+ }
1034
+
1035
+ var head = document.getElementsByTagName('head')[0];
1036
+ head.insertBefore(self.script, head.firstChild);
1037
+ if (self.errorScript) {
1038
+ head.insertBefore(self.errorScript, self.script.nextSibling);
793
1039
  }
794
1040
  };
795
1041
 
796
- CallbackRegistry.prototype._prefix = function(eventName) {
797
- return "_" + eventName;
1042
+ /** Cleans up the DOM remains of the script request. */
1043
+ prototype.cleanup = function() {
1044
+ if (this.script) {
1045
+ this.script.onload = this.script.onerror = null;
1046
+ this.script.onreadystatechange = null;
1047
+ }
1048
+ if (this.script && this.script.parentNode) {
1049
+ this.script.parentNode.removeChild(this.script);
1050
+ }
1051
+ if (this.errorScript && this.errorScript.parentNode) {
1052
+ this.errorScript.parentNode.removeChild(this.errorScript);
1053
+ }
1054
+ this.script = null;
1055
+ this.errorScript = null;
798
1056
  };
799
1057
 
800
- Pusher.EventsDispatcher = EventsDispatcher;
1058
+ Pusher.ScriptRequest = ScriptRequest;
801
1059
  }).call(this);
802
1060
 
803
1061
  ;(function() {
804
1062
  /** Handles loading dependency files.
1063
+ *
1064
+ * Dependency loaders don't remember whether a resource has been loaded or
1065
+ * not. It is caller's responsibility to make sure the resource is not loaded
1066
+ * twice. This is because it's impossible to detect resource loading status
1067
+ * without knowing its content.
805
1068
  *
806
1069
  * Options:
807
1070
  * - cdn_http - url to HTTP CND
@@ -813,8 +1076,8 @@
813
1076
  */
814
1077
  function DependencyLoader(options) {
815
1078
  this.options = options;
1079
+ this.receivers = options.receivers || Pusher.ScriptReceivers;
816
1080
  this.loading = {};
817
- this.loaded = {};
818
1081
  }
819
1082
  var prototype = DependencyLoader.prototype;
820
1083
 
@@ -826,23 +1089,30 @@
826
1089
  prototype.load = function(name, callback) {
827
1090
  var self = this;
828
1091
 
829
- if (this.loaded[name]) {
830
- callback();
831
- } else if (this.loading[name] && this.loading[name].length > 0) {
832
- this.loading[name].push(callback);
1092
+ if (self.loading[name] && self.loading[name].length > 0) {
1093
+ self.loading[name].push(callback);
833
1094
  } else {
834
- this.loading[name] = [callback];
1095
+ self.loading[name] = [callback];
835
1096
 
836
- require(this.getPath(name), function() {
837
- self.loaded[name] = true;
1097
+ var request = new Pusher.ScriptRequest(self.getPath(name));
1098
+ var receiver = self.receivers.create(function(error) {
1099
+ self.receivers.remove(receiver);
838
1100
 
839
1101
  if (self.loading[name]) {
840
- for (var i = 0; i < self.loading[name].length; i++) {
841
- self.loading[name][i]();
842
- }
1102
+ var callbacks = self.loading[name];
843
1103
  delete self.loading[name];
1104
+
1105
+ var successCallback = function(wasSuccessful) {
1106
+ if (!wasSuccessful) {
1107
+ request.cleanup();
1108
+ }
1109
+ };
1110
+ for (var i = 0; i < callbacks.length; i++) {
1111
+ callbacks[i](error, successCallback);
1112
+ }
844
1113
  }
845
1114
  });
1115
+ request.send(receiver);
846
1116
  }
847
1117
  };
848
1118
 
@@ -852,7 +1122,7 @@
852
1122
  */
853
1123
  prototype.getRoot = function(options) {
854
1124
  var cdn;
855
- var protocol = Pusher.Util.getDocumentLocation().protocol;
1125
+ var protocol = Pusher.Util.getDocument().location.protocol;
856
1126
  if ((options && options.encrypted) || protocol === "https:") {
857
1127
  cdn = this.options.cdn_https;
858
1128
  } else {
@@ -871,51 +1141,21 @@
871
1141
  return this.getRoot(options) + '/' + name + this.options.suffix + '.js';
872
1142
  };
873
1143
 
874
- function handleScriptLoaded(elem, callback) {
875
- if (Pusher.Util.getDocument().addEventListener) {
876
- elem.addEventListener('load', callback, false);
877
- } else {
878
- elem.attachEvent('onreadystatechange', function () {
879
- if (elem.readyState === 'loaded' || elem.readyState === 'complete') {
880
- callback();
881
- }
882
- });
883
- }
884
- }
885
-
886
- function require(src, callback) {
887
- var document = Pusher.Util.getDocument();
888
- var head = document.getElementsByTagName('head')[0];
889
- var script = document.createElement('script');
890
-
891
- script.setAttribute('src', src);
892
- script.setAttribute("type","text/javascript");
893
- script.setAttribute('async', true);
894
-
895
- handleScriptLoaded(script, function() {
896
- // workaround for an Opera issue
897
- setTimeout(callback, 0);
898
- });
899
-
900
- head.appendChild(script);
901
- }
902
-
903
1144
  Pusher.DependencyLoader = DependencyLoader;
904
1145
  }).call(this);
905
1146
 
906
1147
  ;(function() {
1148
+ Pusher.DependenciesReceivers = new Pusher.ScriptReceiverFactory(
1149
+ "_pusher_dependencies", "Pusher.DependenciesReceivers"
1150
+ );
907
1151
  Pusher.Dependencies = new Pusher.DependencyLoader({
908
1152
  cdn_http: Pusher.cdn_http,
909
1153
  cdn_https: Pusher.cdn_https,
910
1154
  version: Pusher.VERSION,
911
- suffix: Pusher.dependency_suffix
1155
+ suffix: Pusher.dependency_suffix,
1156
+ receivers: Pusher.DependenciesReceivers
912
1157
  });
913
1158
 
914
- // Support Firefox versions which prefix WebSocket
915
- if (!window.WebSocket && window.MozWebSocket) {
916
- window.WebSocket = window.MozWebSocket;
917
- }
918
-
919
1159
  function initialize() {
920
1160
  Pusher.ready();
921
1161
  }
@@ -996,98 +1236,56 @@
996
1236
  }).call(this);
997
1237
 
998
1238
  (function() {
999
-
1000
- function JSONPRequest(options) {
1001
- this.options = options;
1239
+ /** Sends data via JSONP.
1240
+ *
1241
+ * Data is a key-value map. Its values are JSON-encoded and then passed
1242
+ * through base64. Finally, keys and encoded values are appended to the query
1243
+ * string.
1244
+ *
1245
+ * The class itself does not guarantee raising errors on failures, as it's not
1246
+ * possible to support such feature on all browsers. Instead, JSONP endpoint
1247
+ * should call back in a way that's easy to distinguish from browser calls,
1248
+ * for example by passing a second argument to the receiver.
1249
+ *
1250
+ * @param {String} url
1251
+ * @param {Object} data key-value map of data to be submitted
1252
+ */
1253
+ function JSONPRequest(url, data) {
1254
+ this.url = url;
1255
+ this.data = data;
1002
1256
  }
1003
-
1004
- JSONPRequest.send = function(options, callback) {
1005
- var request = new Pusher.JSONPRequest({
1006
- url: options.url,
1007
- receiver: options.receiverName,
1008
- tagPrefix: options.tagPrefix
1009
- });
1010
- var id = options.receiver.register(function(error, result) {
1011
- request.cleanup();
1012
- callback(error, result);
1013
- });
1014
-
1015
- return request.send(id, options.data, function(error) {
1016
- var callback = options.receiver.unregister(id);
1017
- if (callback) {
1018
- callback(error);
1019
- }
1020
- });
1021
- };
1022
-
1023
1257
  var prototype = JSONPRequest.prototype;
1024
1258
 
1025
- prototype.send = function(id, data, callback) {
1026
- if (this.script) {
1027
- return false;
1259
+ /** Sends the actual JSONP request.
1260
+ *
1261
+ * @param {ScriptReceiver} receiver
1262
+ */
1263
+ prototype.send = function(receiver) {
1264
+ if (this.request) {
1265
+ return;
1028
1266
  }
1029
1267
 
1030
- var tagPrefix = this.options.tagPrefix || "_pusher_jsonp_";
1031
-
1032
- var params = Pusher.Util.extend(
1033
- {}, data, { receiver: this.options.receiver }
1034
- );
1268
+ var params = Pusher.Util.filterObject(this.data, function(value) {
1269
+ return value !== undefined;
1270
+ });
1035
1271
  var query = Pusher.Util.map(
1036
- Pusher.Util.flatten(
1037
- encodeData(
1038
- Pusher.Util.filterObject(params, function(value) {
1039
- return value !== undefined;
1040
- })
1041
- )
1042
- ),
1272
+ Pusher.Util.flatten(encodeParamsObject(params)),
1043
1273
  Pusher.Util.method("join", "=")
1044
1274
  ).join("&");
1275
+ var url = this.url + "/" + receiver.number + "?" + query;
1045
1276
 
1046
- this.script = document.createElement("script");
1047
- this.script.id = tagPrefix + id;
1048
- this.script.src = this.options.url + "/" + id + "?" + query;
1049
- this.script.type = "text/javascript";
1050
- this.script.charset = "UTF-8";
1051
- this.script.onerror = this.script.onload = callback;
1052
-
1053
- // Opera<11.6 hack for missing onerror callback
1054
- if (this.script.async === undefined && document.attachEvent) {
1055
- if (/opera/i.test(navigator.userAgent)) {
1056
- var receiverName = this.options.receiver || "Pusher.JSONP.receive";
1057
- this.errorScript = document.createElement("script");
1058
- this.errorScript.text = receiverName + "(" + id + ", true);";
1059
- this.script.async = this.errorScript.async = false;
1060
- }
1061
- }
1062
-
1063
- var self = this;
1064
- this.script.onreadystatechange = function() {
1065
- if (self.script && /loaded|complete/.test(self.script.readyState)) {
1066
- callback(true);
1067
- }
1068
- };
1069
-
1070
- var head = document.getElementsByTagName('head')[0];
1071
- head.insertBefore(this.script, head.firstChild);
1072
- if (this.errorScript) {
1073
- head.insertBefore(this.errorScript, this.script.nextSibling);
1074
- }
1075
-
1076
- return true;
1277
+ this.request = new Pusher.ScriptRequest(url);
1278
+ this.request.send(receiver);
1077
1279
  };
1078
1280
 
1281
+ /** Cleans up the DOM remains of the JSONP request. */
1079
1282
  prototype.cleanup = function() {
1080
- if (this.script && this.script.parentNode) {
1081
- this.script.parentNode.removeChild(this.script);
1082
- this.script = null;
1083
- }
1084
- if (this.errorScript && this.errorScript.parentNode) {
1085
- this.errorScript.parentNode.removeChild(this.errorScript);
1086
- this.errorScript = null;
1283
+ if (this.request) {
1284
+ this.request.cleanup();
1087
1285
  }
1088
1286
  };
1089
1287
 
1090
- function encodeData(data) {
1288
+ function encodeParamsObject(data) {
1091
1289
  return Pusher.Util.mapObject(data, function(value) {
1092
1290
  if (typeof value === "object") {
1093
1291
  value = JSON.stringify(value);
@@ -1097,45 +1295,6 @@
1097
1295
  }
1098
1296
 
1099
1297
  Pusher.JSONPRequest = JSONPRequest;
1100
-
1101
- }).call(this);
1102
-
1103
- (function() {
1104
-
1105
- function JSONPReceiver() {
1106
- this.lastId = 0;
1107
- this.callbacks = {};
1108
- }
1109
-
1110
- var prototype = JSONPReceiver.prototype;
1111
-
1112
- prototype.register = function(callback) {
1113
- this.lastId++;
1114
- var id = this.lastId;
1115
- this.callbacks[id] = callback;
1116
- return id;
1117
- };
1118
-
1119
- prototype.unregister = function(id) {
1120
- if (this.callbacks[id]) {
1121
- var callback = this.callbacks[id];
1122
- delete this.callbacks[id];
1123
- return callback;
1124
- } else {
1125
- return null;
1126
- }
1127
- };
1128
-
1129
- prototype.receive = function(id, error, data) {
1130
- var callback = this.unregister(id);
1131
- if (callback) {
1132
- callback(error, data);
1133
- }
1134
- };
1135
-
1136
- Pusher.JSONPReceiver = JSONPReceiver;
1137
- Pusher.JSONP = new JSONPReceiver();
1138
-
1139
1298
  }).call(this);
1140
1299
 
1141
1300
  (function() {
@@ -1155,12 +1314,9 @@
1155
1314
  Timeline.DEBUG = 7;
1156
1315
 
1157
1316
  prototype.log = function(level, event) {
1158
- if (this.options.level === undefined || level <= this.options.level) {
1317
+ if (level <= this.options.level) {
1159
1318
  this.events.push(
1160
- Pusher.Util.extend({}, event, {
1161
- timestamp: Pusher.Util.now(),
1162
- level: (level !== Timeline.INFO ? level : undefined)
1163
- })
1319
+ Pusher.Util.extend({}, event, { timestamp: Pusher.Util.now() })
1164
1320
  );
1165
1321
  if (this.options.limit && this.events.length > this.options.limit) {
1166
1322
  this.events.shift();
@@ -1193,6 +1349,7 @@
1193
1349
  key: self.key,
1194
1350
  lib: "js",
1195
1351
  version: self.options.version,
1352
+ cluster: self.options.cluster,
1196
1353
  features: self.options.features,
1197
1354
  timeline: self.events
1198
1355
  }, self.options.params);
@@ -1226,22 +1383,21 @@
1226
1383
  var prototype = TimelineSender.prototype;
1227
1384
 
1228
1385
  prototype.send = function(encrypted, callback) {
1229
- if (this.timeline.isEmpty()) {
1386
+ var self = this;
1387
+
1388
+ if (self.timeline.isEmpty()) {
1230
1389
  return;
1231
1390
  }
1232
1391
 
1233
- var self = this;
1234
- var scheme = "http" + (encrypted ? "s" : "") + "://";
1235
-
1236
1392
  var sendJSONP = function(data, callback) {
1237
- var params = {
1238
- data: Pusher.Util.filterObject(data, function(v) {
1239
- return v !== undefined;
1240
- }),
1241
- url: scheme + (self.host || self.options.host) + self.options.path,
1242
- receiver: Pusher.JSONP
1243
- };
1244
- return Pusher.JSONPRequest.send(params, function(error, result) {
1393
+ var scheme = "http" + (encrypted ? "s" : "") + "://";
1394
+ var url = scheme + (self.host || self.options.host) + self.options.path;
1395
+ var request = new Pusher.JSONPRequest(url, data);
1396
+
1397
+ var receiver = Pusher.ScriptReceivers.create(function(error, result) {
1398
+ Pusher.ScriptReceivers.remove(receiver);
1399
+ request.cleanup();
1400
+
1245
1401
  if (result && result.host) {
1246
1402
  self.host = result.host;
1247
1403
  }
@@ -1249,6 +1405,7 @@
1249
1405
  callback(error, result);
1250
1406
  }
1251
1407
  });
1408
+ request.send(receiver);
1252
1409
  };
1253
1410
  self.timeline.send(sendJSONP, callback);
1254
1411
  };
@@ -1359,9 +1516,13 @@
1359
1516
  if (info && info.timestamp + this.ttl >= Pusher.Util.now()) {
1360
1517
  var transport = this.transports[info.transport];
1361
1518
  if (transport) {
1362
- this.timeline.info({ cached: true, transport: info.transport });
1519
+ this.timeline.info({
1520
+ cached: true,
1521
+ transport: info.transport,
1522
+ latency: info.latency
1523
+ });
1363
1524
  strategies.push(new Pusher.SequentialStrategy([transport], {
1364
- timeout: info.latency * 2,
1525
+ timeout: info.latency * 2 + 1000,
1365
1526
  failFast: true
1366
1527
  }));
1367
1528
  }
@@ -1693,7 +1854,9 @@
1693
1854
  * @returns {Boolean}
1694
1855
  */
1695
1856
  prototype.isSupported = function() {
1696
- return this.transport.isSupported();
1857
+ return this.transport.isSupported({
1858
+ encrypted: this.options.encrypted
1859
+ });
1697
1860
  };
1698
1861
 
1699
1862
  /** Launches a connection attempt and returns a strategy runner.
@@ -1791,17 +1954,65 @@
1791
1954
  Pusher.TransportStrategy = TransportStrategy;
1792
1955
  }).call(this);
1793
1956
 
1794
- ;(function() {
1795
- /** Handles common logic for all transports.
1957
+ (function() {
1958
+ function getGenericURL(baseScheme, params, path) {
1959
+ var scheme = baseScheme + (params.encrypted ? "s" : "");
1960
+ var host = params.encrypted ? params.hostEncrypted : params.hostUnencrypted;
1961
+ return scheme + "://" + host + path;
1962
+ }
1963
+
1964
+ function getGenericPath(key, queryString) {
1965
+ var path = "/app/" + key;
1966
+ var query =
1967
+ "?protocol=" + Pusher.PROTOCOL +
1968
+ "&client=js" +
1969
+ "&version=" + Pusher.VERSION +
1970
+ (queryString ? ("&" + queryString) : "");
1971
+ return path + query;
1972
+ }
1973
+
1974
+ /** URL schemes for different transport types. */
1975
+ Pusher.URLSchemes = {
1976
+ /** Standard WebSocket URL scheme. */
1977
+ ws: {
1978
+ getInitial: function(key, params) {
1979
+ return getGenericURL("ws", params, getGenericPath(key, "flash=false"));
1980
+ }
1981
+ },
1982
+ /** URL scheme for Flash. Same as WebSocket, but with a flash parameter. */
1983
+ flash: {
1984
+ getInitial: function(key, params) {
1985
+ return getGenericURL("ws", params, getGenericPath(key, "flash=true"));
1986
+ }
1987
+ },
1988
+ /** SockJS URL scheme. Supplies the path separately from the initial URL. */
1989
+ sockjs: {
1990
+ getInitial: function(key, params) {
1991
+ return getGenericURL("http", params, params.httpPath || "/pusher", "");
1992
+ },
1993
+ getPath: function(key, params) {
1994
+ return getGenericPath(key);
1995
+ }
1996
+ },
1997
+ /** URL scheme for HTTP transports. Basically, WS scheme with a prefix. */
1998
+ http: {
1999
+ getInitial: function(key, params) {
2000
+ var path = (params.httpPath || "/pusher") + getGenericPath(key);
2001
+ return getGenericURL("http", params, path);
2002
+ }
2003
+ }
2004
+ };
2005
+ }).call(this);
2006
+
2007
+ (function() {
2008
+ /** Provides universal API for transport connections.
1796
2009
  *
1797
- * Transport is a low-level connection object that wraps a connection method
2010
+ * Transport connection is a low-level object that wraps a connection method
1798
2011
  * and exposes a simple evented interface for the connection state and
1799
2012
  * messaging. It does not implement Pusher-specific WebSocket protocol.
1800
2013
  *
1801
2014
  * Additionally, it fetches resources needed for transport to work and exposes
1802
- * an interface for querying transport support and its features.
1803
- *
1804
- * This is an abstract class, please do not instantiate it.
2015
+ * an interface for querying transport features.
1805
2016
  *
1806
2017
  * States:
1807
2018
  * - new - initial state after constructing the object
@@ -1822,40 +2033,37 @@
1822
2033
  * @param {String} key application key
1823
2034
  * @param {Object} options
1824
2035
  */
1825
- function AbstractTransport(name, priority, key, options) {
2036
+ function TransportConnection(hooks, name, priority, key, options) {
1826
2037
  Pusher.EventsDispatcher.call(this);
1827
2038
 
2039
+ this.hooks = hooks;
1828
2040
  this.name = name;
1829
2041
  this.priority = priority;
1830
2042
  this.key = key;
2043
+ this.options = options;
2044
+
1831
2045
  this.state = "new";
1832
2046
  this.timeline = options.timeline;
1833
2047
  this.activityTimeout = options.activityTimeout;
1834
2048
  this.id = this.timeline.generateUniqueID();
1835
-
1836
- this.options = {
1837
- encrypted: Boolean(options.encrypted),
1838
- hostUnencrypted: options.hostUnencrypted,
1839
- hostEncrypted: options.hostEncrypted
1840
- };
1841
2049
  }
1842
- var prototype = AbstractTransport.prototype;
2050
+ var prototype = TransportConnection.prototype;
1843
2051
  Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype);
1844
2052
 
1845
- /** Checks whether the transport is supported in the browser.
2053
+ /** Checks whether the transport handles activity checks by itself.
1846
2054
  *
1847
- * @returns {Boolean}
2055
+ * @return {Boolean}
1848
2056
  */
1849
- AbstractTransport.isSupported = function() {
1850
- return false;
2057
+ prototype.handlesActivityChecks = function() {
2058
+ return Boolean(this.hooks.handlesActivityChecks);
1851
2059
  };
1852
2060
 
1853
- /** Checks whether the transport handles ping/pong on itself.
2061
+ /** Checks whether the transport supports the ping/pong API.
1854
2062
  *
1855
2063
  * @return {Boolean}
1856
2064
  */
1857
2065
  prototype.supportsPing = function() {
1858
- return false;
2066
+ return Boolean(this.hooks.supportsPing);
1859
2067
  };
1860
2068
 
1861
2069
  /** Initializes the transport.
@@ -1863,12 +2071,35 @@
1863
2071
  * Fetches resources if needed and then transitions to initialized.
1864
2072
  */
1865
2073
  prototype.initialize = function() {
1866
- this.timeline.info(this.buildTimelineMessage({
1867
- transport: this.name + (this.options.encrypted ? "s" : "")
2074
+ var self = this;
2075
+
2076
+ self.timeline.info(self.buildTimelineMessage({
2077
+ transport: self.name + (self.options.encrypted ? "s" : "")
1868
2078
  }));
1869
- this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
1870
2079
 
1871
- this.changeState("initialized");
2080
+ if (self.hooks.beforeInitialize) {
2081
+ self.hooks.beforeInitialize();
2082
+ }
2083
+
2084
+ if (self.hooks.isInitialized()) {
2085
+ self.changeState("initialized");
2086
+ } else if (self.hooks.file) {
2087
+ self.changeState("initializing");
2088
+ Pusher.Dependencies.load(self.hooks.file, function(error, callback) {
2089
+ if (self.hooks.isInitialized()) {
2090
+ self.changeState("initialized");
2091
+ callback(true);
2092
+ } else {
2093
+ if (error) {
2094
+ self.onError(error);
2095
+ }
2096
+ self.onClose();
2097
+ callback(false);
2098
+ }
2099
+ });
2100
+ } else {
2101
+ self.onClose();
2102
+ }
1872
2103
  };
1873
2104
 
1874
2105
  /** Tries to establish a connection.
@@ -1876,20 +2107,16 @@
1876
2107
  * @returns {Boolean} false if transport is in invalid state
1877
2108
  */
1878
2109
  prototype.connect = function() {
1879
- var url = this.getURL(this.key, this.options);
1880
- this.timeline.debug(this.buildTimelineMessage({
1881
- method: "connect",
1882
- url: url
1883
- }));
2110
+ var self = this;
1884
2111
 
1885
- if (this.socket || this.state !== "initialized") {
2112
+ if (self.socket || self.state !== "initialized") {
1886
2113
  return false;
1887
2114
  }
1888
2115
 
2116
+ var url = self.hooks.urls.getInitial(self.key, self.options);
1889
2117
  try {
1890
- this.socket = this.createSocket(url);
2118
+ self.socket = self.hooks.getSocket(url, self.options);
1891
2119
  } catch (e) {
1892
- var self = this;
1893
2120
  Pusher.Util.defer(function() {
1894
2121
  self.onError(e);
1895
2122
  self.changeState("closed");
@@ -1897,10 +2124,10 @@
1897
2124
  return false;
1898
2125
  }
1899
2126
 
1900
- this.bindListeners();
2127
+ self.bindListeners();
1901
2128
 
1902
- Pusher.debug("Connecting", { transport: this.name, url: url });
1903
- this.changeState("connecting");
2129
+ Pusher.debug("Connecting", { transport: self.name, url: url });
2130
+ self.changeState("connecting");
1904
2131
  return true;
1905
2132
  };
1906
2133
 
@@ -1909,8 +2136,6 @@
1909
2136
  * @return {Boolean} true if there was a connection to close
1910
2137
  */
1911
2138
  prototype.close = function() {
1912
- this.timeline.debug(this.buildTimelineMessage({ method: "close" }));
1913
-
1914
2139
  if (this.socket) {
1915
2140
  this.socket.close();
1916
2141
  return true;
@@ -1925,38 +2150,46 @@
1925
2150
  * @return {Boolean} true only when in the "open" state
1926
2151
  */
1927
2152
  prototype.send = function(data) {
1928
- this.timeline.debug(this.buildTimelineMessage({
1929
- method: "send",
1930
- data: data
1931
- }));
2153
+ var self = this;
1932
2154
 
1933
- if (this.state === "open") {
2155
+ if (self.state === "open") {
1934
2156
  // Workaround for MobileSafari bug (see https://gist.github.com/2052006)
1935
- var self = this;
1936
- setTimeout(function() {
2157
+ Pusher.Util.defer(function() {
1937
2158
  if (self.socket) {
1938
2159
  self.socket.send(data);
1939
2160
  }
1940
- }, 0);
2161
+ });
1941
2162
  return true;
1942
2163
  } else {
1943
2164
  return false;
1944
2165
  }
1945
2166
  };
1946
2167
 
1947
- /** @protected */
2168
+ /** Sends a ping if the connection is open and transport supports it. */
2169
+ prototype.ping = function() {
2170
+ if (this.state === "open" && this.supportsPing()) {
2171
+ this.socket.ping();
2172
+ }
2173
+ };
2174
+
2175
+ /** @private */
1948
2176
  prototype.onOpen = function() {
2177
+ if (this.hooks.beforeOpen) {
2178
+ this.hooks.beforeOpen(
2179
+ this.socket, this.hooks.urls.getPath(this.key, this.options)
2180
+ );
2181
+ }
1949
2182
  this.changeState("open");
1950
2183
  this.socket.onopen = undefined;
1951
2184
  };
1952
2185
 
1953
- /** @protected */
2186
+ /** @private */
1954
2187
  prototype.onError = function(error) {
1955
2188
  this.emit("error", { type: 'WebSocketError', error: error });
1956
- this.timeline.error(this.buildTimelineMessage({}));
2189
+ this.timeline.error(this.buildTimelineMessage({ error: error.toString() }));
1957
2190
  };
1958
2191
 
1959
- /** @protected */
2192
+ /** @private */
1960
2193
  prototype.onClose = function(closeEvent) {
1961
2194
  if (closeEvent) {
1962
2195
  this.changeState("closed", {
@@ -1967,63 +2200,56 @@
1967
2200
  } else {
1968
2201
  this.changeState("closed");
1969
2202
  }
2203
+ this.unbindListeners();
1970
2204
  this.socket = undefined;
1971
2205
  };
1972
2206
 
1973
- /** @protected */
2207
+ /** @private */
1974
2208
  prototype.onMessage = function(message) {
1975
- this.timeline.debug(this.buildTimelineMessage({ message: message.data }));
1976
2209
  this.emit("message", message);
1977
2210
  };
1978
2211
 
1979
- /** @protected */
1980
- prototype.bindListeners = function() {
1981
- var self = this;
1982
-
1983
- this.socket.onopen = function() { self.onOpen(); };
1984
- this.socket.onerror = function(error) { self.onError(error); };
1985
- this.socket.onclose = function(closeEvent) { self.onClose(closeEvent); };
1986
- this.socket.onmessage = function(message) { self.onMessage(message); };
2212
+ /** @private */
2213
+ prototype.onActivity = function() {
2214
+ this.emit("activity");
1987
2215
  };
1988
2216
 
1989
- /** @protected */
1990
- prototype.createSocket = function(url) {
1991
- return null;
1992
- };
2217
+ /** @private */
2218
+ prototype.bindListeners = function() {
2219
+ var self = this;
1993
2220
 
1994
- /** @protected */
1995
- prototype.getScheme = function() {
1996
- return this.options.encrypted ? "wss" : "ws";
1997
- };
2221
+ self.socket.onopen = function() {
2222
+ self.onOpen();
2223
+ };
2224
+ self.socket.onerror = function(error) {
2225
+ self.onError(error);
2226
+ };
2227
+ self.socket.onclose = function(closeEvent) {
2228
+ self.onClose(closeEvent);
2229
+ };
2230
+ self.socket.onmessage = function(message) {
2231
+ self.onMessage(message);
2232
+ };
1998
2233
 
1999
- /** @protected */
2000
- prototype.getBaseURL = function() {
2001
- var host;
2002
- if (this.options.encrypted) {
2003
- host = this.options.hostEncrypted;
2004
- } else {
2005
- host = this.options.hostUnencrypted;
2234
+ if (self.supportsPing()) {
2235
+ self.socket.onactivity = function() { self.onActivity(); };
2006
2236
  }
2007
- return this.getScheme() + "://" + host;
2008
- };
2009
-
2010
- /** @protected */
2011
- prototype.getPath = function() {
2012
- return "/app/" + this.key;
2013
- };
2014
-
2015
- /** @protected */
2016
- prototype.getQueryString = function() {
2017
- return "?protocol=" + Pusher.PROTOCOL +
2018
- "&client=js&version=" + Pusher.VERSION;
2019
2237
  };
2020
2238
 
2021
- /** @protected */
2022
- prototype.getURL = function() {
2023
- return this.getBaseURL() + this.getPath() + this.getQueryString();
2239
+ /** @private */
2240
+ prototype.unbindListeners = function() {
2241
+ if (this.socket) {
2242
+ this.socket.onopen = undefined;
2243
+ this.socket.onerror = undefined;
2244
+ this.socket.onclose = undefined;
2245
+ this.socket.onmessage = undefined;
2246
+ if (this.supportsPing()) {
2247
+ this.socket.onactivity = undefined;
2248
+ }
2249
+ }
2024
2250
  };
2025
2251
 
2026
- /** @protected */
2252
+ /** @private */
2027
2253
  prototype.changeState = function(state, params) {
2028
2254
  this.state = state;
2029
2255
  this.timeline.info(this.buildTimelineMessage({
@@ -2033,245 +2259,219 @@
2033
2259
  this.emit(state, params);
2034
2260
  };
2035
2261
 
2036
- /** @protected */
2262
+ /** @private */
2037
2263
  prototype.buildTimelineMessage = function(message) {
2038
2264
  return Pusher.Util.extend({ cid: this.id }, message);
2039
2265
  };
2040
2266
 
2041
- Pusher.AbstractTransport = AbstractTransport;
2267
+ Pusher.TransportConnection = TransportConnection;
2042
2268
  }).call(this);
2043
2269
 
2044
- ;(function() {
2045
- /** Transport using Flash to emulate WebSockets.
2046
- *
2047
- * @see AbstractTransport
2048
- */
2049
- function FlashTransport(name, priority, key, options) {
2050
- Pusher.AbstractTransport.call(this, name, priority, key, options);
2051
- }
2052
- var prototype = FlashTransport.prototype;
2053
- Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
2054
-
2055
- /** Creates a new instance of FlashTransport.
2056
- *
2057
- * @param {String} key
2058
- * @param {Object} options
2059
- * @return {FlashTransport}
2060
- */
2061
- FlashTransport.createConnection = function(name, priority, key, options) {
2062
- return new FlashTransport(name, priority, key, options);
2063
- };
2064
-
2065
- /** Checks whether Flash is supported in the browser.
2066
- *
2067
- * It is possible to disable flash by passing an envrionment object with the
2068
- * disableFlash property set to true.
2270
+ (function() {
2271
+ /** Provides interface for transport connection instantiation.
2069
2272
  *
2070
- * @see AbstractTransport.isSupported
2071
- * @param {Object} environment
2072
- * @returns {Boolean}
2073
- */
2074
- FlashTransport.isSupported = function() {
2075
- try {
2076
- return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash'));
2077
- } catch (e) {
2078
- try {
2079
- return Boolean(
2080
- navigator &&
2081
- navigator.mimeTypes &&
2082
- navigator.mimeTypes["application/x-shockwave-flash"] !== undefined
2083
- );
2084
- } catch(e) {
2085
- return false;
2086
- }
2087
- }
2088
- };
2089
-
2090
- /** Fetches flashfallback dependency if needed.
2273
+ * Takes transport-specific hooks as the only argument, which allow checking
2274
+ * for transport support and creating its connections.
2091
2275
  *
2092
- * Sets WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR to true (if not set before)
2093
- * and WEB_SOCKET_SWF_LOCATION to Pusher's cdn before loading Flash resources.
2276
+ * Supported hooks:
2277
+ * - file - the name of the file to be fetched during initialization
2278
+ * - urls - URL scheme to be used by transport
2279
+ * - handlesActivityCheck - true when the transport handles activity checks
2280
+ * - supportsPing - true when the transport has a ping/activity API
2281
+ * - isSupported - tells whether the transport is supported in the environment
2282
+ * - getSocket - creates a WebSocket-compatible transport socket
2094
2283
  *
2095
- * @see AbstractTransport.prototype.initialize
2096
- */
2097
- prototype.initialize = function() {
2098
- var self = this;
2099
-
2100
- this.timeline.info(this.buildTimelineMessage({
2101
- transport: this.name + (this.options.encrypted ? "s" : "")
2102
- }));
2103
- this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
2104
- this.changeState("initializing");
2105
-
2106
- if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) {
2107
- window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
2108
- }
2109
- window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() +
2110
- "/WebSocketMain.swf";
2111
- Pusher.Dependencies.load("flashfallback", function() {
2112
- self.changeState("initialized");
2113
- });
2114
- };
2115
-
2116
- /** @protected */
2117
- prototype.createSocket = function(url) {
2118
- return new FlashWebSocket(url);
2119
- };
2120
-
2121
- /** @protected */
2122
- prototype.getQueryString = function() {
2123
- return Pusher.AbstractTransport.prototype.getQueryString.call(this) +
2124
- "&flash=true";
2125
- };
2126
-
2127
- Pusher.FlashTransport = FlashTransport;
2128
- }).call(this);
2129
-
2130
- ;(function() {
2131
- /** Fallback transport using SockJS.
2284
+ * See transports.js for specific implementations.
2132
2285
  *
2133
- * @see AbstractTransport
2286
+ * @param {Object} hooks object containing all needed transport hooks
2134
2287
  */
2135
- function SockJSTransport(name, priority, key, options) {
2136
- Pusher.AbstractTransport.call(this, name, priority, key, options);
2137
- this.options.ignoreNullOrigin = options.ignoreNullOrigin;
2288
+ function Transport(hooks) {
2289
+ this.hooks = hooks;
2138
2290
  }
2139
- var prototype = SockJSTransport.prototype;
2140
- Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
2291
+ var prototype = Transport.prototype;
2141
2292
 
2142
- /** Creates a new instance of SockJSTransport.
2293
+ /** Returns whether the transport is supported in the environment.
2143
2294
  *
2144
- * @param {String} key
2145
- * @param {Object} options
2146
- * @return {SockJSTransport}
2295
+ * @param {Object} environment the environment details (encryption, settings)
2296
+ * @returns {Boolean} true when the transport is supported
2147
2297
  */
2148
- SockJSTransport.createConnection = function(name, priority, key, options) {
2149
- return new SockJSTransport(name, priority, key, options);
2298
+ prototype.isSupported = function(environment) {
2299
+ return this.hooks.isSupported(environment);
2150
2300
  };
2151
2301
 
2152
- /** Assumes that SockJS is always supported.
2302
+ /** Creates a transport connection.
2153
2303
  *
2154
- * @returns {Boolean} always true
2304
+ * @param {String} name
2305
+ * @param {Number} priority
2306
+ * @param {String} key the application key
2307
+ * @param {Object} options
2308
+ * @returns {TransportConnection}
2155
2309
  */
2156
- SockJSTransport.isSupported = function() {
2157
- return true;
2310
+ prototype.createConnection = function(name, priority, key, options) {
2311
+ return new Pusher.TransportConnection(
2312
+ this.hooks, name, priority, key, options
2313
+ );
2158
2314
  };
2159
2315
 
2160
- /** Fetches sockjs dependency if needed.
2161
- *
2162
- * @see AbstractTransport.prototype.initialize
2163
- */
2164
- prototype.initialize = function() {
2165
- var self = this;
2166
-
2167
- this.timeline.info(this.buildTimelineMessage({
2168
- transport: this.name + (this.options.encrypted ? "s" : "")
2169
- }));
2170
- this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
2171
-
2172
- this.changeState("initializing");
2173
- Pusher.Dependencies.load("sockjs", function() {
2174
- self.changeState("initialized");
2175
- });
2176
- };
2316
+ Pusher.Transport = Transport;
2317
+ }).call(this);
2177
2318
 
2178
- /** Always returns true, since SockJS handles ping on its own.
2319
+ (function() {
2320
+ /** WebSocket transport.
2179
2321
  *
2180
- * @returns {Boolean} always true
2322
+ * Uses native WebSocket implementation, including MozWebSocket supported by
2323
+ * earlier Firefox versions.
2181
2324
  */
2182
- prototype.supportsPing = function() {
2183
- return true;
2184
- };
2325
+ Pusher.WSTransport = new Pusher.Transport({
2326
+ urls: Pusher.URLSchemes.ws,
2327
+ handlesActivityChecks: false,
2328
+ supportsPing: false,
2185
2329
 
2186
- /** @protected */
2187
- prototype.createSocket = function(url) {
2188
- return new SockJS(url, null, {
2189
- js_path: Pusher.Dependencies.getPath("sockjs", {
2190
- encrypted: this.options.encrypted
2191
- }),
2192
- ignore_null_origin: this.options.ignoreNullOrigin
2193
- });
2194
- };
2330
+ isInitialized: function() {
2331
+ return Boolean(window.WebSocket || window.MozWebSocket);
2332
+ },
2333
+ isSupported: function() {
2334
+ return Boolean(window.WebSocket || window.MozWebSocket);
2335
+ },
2336
+ getSocket: function(url) {
2337
+ var Constructor = window.WebSocket || window.MozWebSocket;
2338
+ return new Constructor(url);
2339
+ }
2340
+ });
2195
2341
 
2196
- /** @protected */
2197
- prototype.getScheme = function() {
2198
- return this.options.encrypted ? "https" : "http";
2199
- };
2342
+ /** Flash transport using the WebSocket protocol. */
2343
+ Pusher.FlashTransport = new Pusher.Transport({
2344
+ file: "flashfallback",
2345
+ urls: Pusher.URLSchemes.flash,
2346
+ handlesActivityChecks: false,
2347
+ supportsPing: false,
2200
2348
 
2201
- /** @protected */
2202
- prototype.getPath = function() {
2203
- return this.options.httpPath || "/pusher";
2204
- };
2349
+ isSupported: function() {
2350
+ try {
2351
+ return Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash'));
2352
+ } catch (e1) {
2353
+ try {
2354
+ var nav = Pusher.Util.getNavigator();
2355
+ return Boolean(
2356
+ nav &&
2357
+ nav.mimeTypes &&
2358
+ nav.mimeTypes["application/x-shockwave-flash"] !== undefined
2359
+ );
2360
+ } catch (e2) {
2361
+ return false;
2362
+ }
2363
+ }
2364
+ },
2365
+ beforeInitialize: function() {
2366
+ if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) {
2367
+ window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
2368
+ }
2369
+ window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() +
2370
+ "/WebSocketMain.swf";
2371
+ },
2372
+ isInitialized: function() {
2373
+ return window.FlashWebSocket !== undefined;
2374
+ },
2375
+ getSocket: function(url) {
2376
+ return new FlashWebSocket(url);
2377
+ }
2378
+ });
2205
2379
 
2206
- /** @protected */
2207
- prototype.getQueryString = function() {
2208
- return "";
2209
- };
2380
+ /** SockJS transport. */
2381
+ Pusher.SockJSTransport = new Pusher.Transport({
2382
+ file: "sockjs",
2383
+ urls: Pusher.URLSchemes.sockjs,
2384
+ handlesActivityChecks: true,
2385
+ supportsPing: false,
2210
2386
 
2211
- /** Handles opening a SockJS connection to Pusher.
2212
- *
2213
- * Since SockJS does not handle custom paths, we send it immediately after
2214
- * establishing the connection.
2215
- *
2216
- * @protected
2217
- */
2218
- prototype.onOpen = function() {
2219
- this.socket.send(JSON.stringify({
2220
- path: Pusher.AbstractTransport.prototype.getPath.call(this) +
2221
- Pusher.AbstractTransport.prototype.getQueryString.call(this)
2222
- }));
2223
- this.changeState("open");
2224
- this.socket.onopen = undefined;
2387
+ isSupported: function() {
2388
+ return true;
2389
+ },
2390
+ isInitialized: function() {
2391
+ return window.SockJS !== undefined;
2392
+ },
2393
+ getSocket: function(url, options) {
2394
+ return new SockJS(url, null, {
2395
+ js_path: Pusher.Dependencies.getPath("sockjs", {
2396
+ encrypted: options.encrypted
2397
+ }),
2398
+ ignore_null_origin: options.ignoreNullOrigin
2399
+ });
2400
+ },
2401
+ beforeOpen: function(socket, path) {
2402
+ socket.send(JSON.stringify({
2403
+ path: path
2404
+ }));
2405
+ }
2406
+ });
2407
+
2408
+ var httpConfiguration = {
2409
+ urls: Pusher.URLSchemes.http,
2410
+ handlesActivityChecks: false,
2411
+ supportsPing: true,
2412
+ isInitialized: function() {
2413
+ return Boolean(Pusher.HTTP.Socket);
2414
+ }
2225
2415
  };
2226
2416
 
2227
- Pusher.SockJSTransport = SockJSTransport;
2417
+ var streamingConfiguration = Pusher.Util.extend(
2418
+ { getSocket: function(url) {
2419
+ return Pusher.HTTP.getStreamingSocket(url);
2420
+ }
2421
+ },
2422
+ httpConfiguration
2423
+ );
2424
+ var pollingConfiguration = Pusher.Util.extend(
2425
+ { getSocket: function(url) {
2426
+ return Pusher.HTTP.getPollingSocket(url);
2427
+ }
2428
+ },
2429
+ httpConfiguration
2430
+ );
2431
+
2432
+ var xhrConfiguration = {
2433
+ file: "xhr",
2434
+ isSupported: Pusher.Util.isXHRSupported
2435
+ };
2436
+ var xdrConfiguration = {
2437
+ file: "xdr",
2438
+ isSupported: function(environment) {
2439
+ return Pusher.Util.isXDRSupported(environment.encrypted);
2440
+ }
2441
+ };
2442
+
2443
+ /** HTTP streaming transport using CORS-enabled XMLHttpRequest. */
2444
+ Pusher.XHRStreamingTransport = new Pusher.Transport(
2445
+ Pusher.Util.extend({}, streamingConfiguration, xhrConfiguration)
2446
+ );
2447
+ /** HTTP streaming transport using XDomainRequest (IE 8,9). */
2448
+ Pusher.XDRStreamingTransport = new Pusher.Transport(
2449
+ Pusher.Util.extend({}, streamingConfiguration, xdrConfiguration)
2450
+ );
2451
+ /** HTTP long-polling transport using CORS-enabled XMLHttpRequest. */
2452
+ Pusher.XHRPollingTransport = new Pusher.Transport(
2453
+ Pusher.Util.extend({}, pollingConfiguration, xhrConfiguration)
2454
+ );
2455
+ /** HTTP long-polling transport using XDomainRequest (IE 8,9). */
2456
+ Pusher.XDRPollingTransport = new Pusher.Transport(
2457
+ Pusher.Util.extend({}, pollingConfiguration, xdrConfiguration)
2458
+ );
2228
2459
  }).call(this);
2229
2460
 
2230
2461
  ;(function() {
2231
- /** WebSocket transport.
2232
- *
2233
- * @see AbstractTransport
2234
- */
2235
- function WSTransport(name, priority, key, options) {
2236
- Pusher.AbstractTransport.call(this, name, priority, key, options);
2237
- }
2238
- var prototype = WSTransport.prototype;
2239
- Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
2240
-
2241
- /** Creates a new instance of WSTransport.
2462
+ /** Creates transport connections monitored by a transport manager.
2242
2463
  *
2243
- * @param {String} key
2244
- * @param {Object} options
2245
- * @return {WSTransport}
2246
- */
2247
- WSTransport.createConnection = function(name, priority, key, options) {
2248
- return new WSTransport(name, priority, key, options);
2249
- };
2250
-
2251
- /** Checks whether the browser supports WebSockets in any form.
2464
+ * When a transport is closed, it might mean the environment does not support
2465
+ * it. It's possible that messages get stuck in an intermediate buffer or
2466
+ * proxies terminate inactive connections. To combat these problems,
2467
+ * assistants monitor the connection lifetime, report unclean exits and
2468
+ * adjust ping timeouts to keep the connection active. The decision to disable
2469
+ * a transport is the manager's responsibility.
2252
2470
  *
2253
- * @returns {Boolean} true if browser supports WebSockets
2471
+ * @param {TransportManager} manager
2472
+ * @param {TransportConnection} transport
2473
+ * @param {Object} options
2254
2474
  */
2255
- WSTransport.isSupported = function() {
2256
- return window.WebSocket !== undefined || window.MozWebSocket !== undefined;
2257
- };
2258
-
2259
- /** @protected */
2260
- prototype.createSocket = function(url) {
2261
- var constructor = window.WebSocket || window.MozWebSocket;
2262
- return new constructor(url);
2263
- };
2264
-
2265
- /** @protected */
2266
- prototype.getQueryString = function() {
2267
- return Pusher.AbstractTransport.prototype.getQueryString.call(this) +
2268
- "&flash=false";
2269
- };
2270
-
2271
- Pusher.WSTransport = WSTransport;
2272
- }).call(this);
2273
-
2274
- ;(function() {
2275
2475
  function AssistantToTheTransportManager(manager, transport, options) {
2276
2476
  this.manager = manager;
2277
2477
  this.transport = transport;
@@ -2281,10 +2481,20 @@
2281
2481
  }
2282
2482
  var prototype = AssistantToTheTransportManager.prototype;
2283
2483
 
2484
+ /** Creates a transport connection.
2485
+ *
2486
+ * This function has the same API as Transport#createConnection.
2487
+ *
2488
+ * @param {String} name
2489
+ * @param {Number} priority
2490
+ * @param {String} key the application key
2491
+ * @param {Object} options
2492
+ * @returns {TransportConnection}
2493
+ */
2284
2494
  prototype.createConnection = function(name, priority, key, options) {
2285
2495
  var self = this;
2286
2496
 
2287
- var options = Pusher.Util.extend({}, options, {
2497
+ options = Pusher.Util.extend({}, options, {
2288
2498
  activityTimeout: self.pingDelay
2289
2499
  });
2290
2500
  var connection = self.transport.createConnection(
@@ -2318,20 +2528,42 @@
2318
2528
  return connection;
2319
2529
  };
2320
2530
 
2321
- prototype.isSupported = function() {
2322
- return this.manager.isAlive() && this.transport.isSupported();
2531
+ /** Returns whether the transport is supported in the environment.
2532
+ *
2533
+ * This function has the same API as Transport#isSupported. Might return false
2534
+ * when the manager decides to kill the transport.
2535
+ *
2536
+ * @param {Object} environment the environment details (encryption, settings)
2537
+ * @returns {Boolean} true when the transport is supported
2538
+ */
2539
+ prototype.isSupported = function(environment) {
2540
+ return this.manager.isAlive() && this.transport.isSupported(environment);
2323
2541
  };
2324
2542
 
2325
2543
  Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager;
2326
2544
  }).call(this);
2327
2545
 
2328
2546
  ;(function() {
2547
+ /** Keeps track of the number of lives left for a transport.
2548
+ *
2549
+ * In the beginning of a session, transports may be assigned a number of
2550
+ * lives. When an AssistantToTheTransportManager instance reports a transport
2551
+ * connection closed uncleanly, the transport loses a life. When the number
2552
+ * of lives drops to zero, the transport gets disabled by its manager.
2553
+ *
2554
+ * @param {Object} options
2555
+ */
2329
2556
  function TransportManager(options) {
2330
2557
  this.options = options || {};
2331
2558
  this.livesLeft = this.options.lives || Infinity;
2332
2559
  }
2333
2560
  var prototype = TransportManager.prototype;
2334
2561
 
2562
+ /** Creates a assistant for the transport.
2563
+ *
2564
+ * @param {Transport} transport
2565
+ * @returns {AssistantToTheTransportManager}
2566
+ */
2335
2567
  prototype.getAssistant = function(transport) {
2336
2568
  return new Pusher.AssistantToTheTransportManager(this, transport, {
2337
2569
  minPingDelay: this.options.minPingDelay,
@@ -2339,10 +2571,15 @@
2339
2571
  });
2340
2572
  };
2341
2573
 
2574
+ /** Returns whether the transport has any lives left.
2575
+ *
2576
+ * @returns {Boolean}
2577
+ */
2342
2578
  prototype.isAlive = function() {
2343
2579
  return this.livesLeft > 0;
2344
2580
  };
2345
2581
 
2582
+ /** Takes one life from the transport. */
2346
2583
  prototype.reportDeath = function() {
2347
2584
  this.livesLeft -= 1;
2348
2585
  };
@@ -2367,7 +2604,11 @@
2367
2604
  var transports = {
2368
2605
  ws: Pusher.WSTransport,
2369
2606
  flash: Pusher.FlashTransport,
2370
- sockjs: Pusher.SockJSTransport
2607
+ sockjs: Pusher.SockJSTransport,
2608
+ xhr_streaming: Pusher.XHRStreamingTransport,
2609
+ xdr_streaming: Pusher.XDRStreamingTransport,
2610
+ xhr_polling: Pusher.XHRPollingTransport,
2611
+ xdr_polling: Pusher.XDRPollingTransport
2371
2612
  };
2372
2613
 
2373
2614
  var UnsupportedStrategy = {
@@ -2396,6 +2637,10 @@
2396
2637
  }
2397
2638
 
2398
2639
  var globalContext = {
2640
+ extend: function(context, first, second) {
2641
+ return [Pusher.Util.extend({}, first, second), context];
2642
+ },
2643
+
2399
2644
  def: function(context, name, value) {
2400
2645
  if (context[name] !== undefined) {
2401
2646
  throw "Redefining symbol " + name;
@@ -2718,12 +2963,12 @@
2718
2963
  var prototype = Connection.prototype;
2719
2964
  Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype);
2720
2965
 
2721
- /** Returns whether used transport handles ping/pong by itself
2966
+ /** Returns whether used transport handles activity checks by itself
2722
2967
  *
2723
- * @returns {Boolean} true if ping is handled by the transport
2968
+ * @returns {Boolean} true if activity checks are handled by the transport
2724
2969
  */
2725
- prototype.supportsPing = function() {
2726
- return this.transport.supportsPing();
2970
+ prototype.handlesActivityChecks = function() {
2971
+ return this.transport.handlesActivityChecks();
2727
2972
  };
2728
2973
 
2729
2974
  /** Sends raw data.
@@ -2750,6 +2995,19 @@
2750
2995
  return this.send(Pusher.Protocol.encodeMessage(message));
2751
2996
  };
2752
2997
 
2998
+ /** Sends a ping message to the server.
2999
+ *
3000
+ * Basing on the underlying transport, it might send either transport's
3001
+ * protocol-specific ping or pusher:ping event.
3002
+ */
3003
+ prototype.ping = function() {
3004
+ if (this.transport.supportsPing()) {
3005
+ this.transport.ping();
3006
+ } else {
3007
+ this.send_event('pusher:ping', {});
3008
+ }
3009
+ };
3010
+
2753
3011
  /** Closes the connection. */
2754
3012
  prototype.close = function() {
2755
3013
  this.transport.close();
@@ -2759,58 +3017,63 @@
2759
3017
  prototype.bindListeners = function() {
2760
3018
  var self = this;
2761
3019
 
2762
- var onMessage = function(m) {
2763
- var message;
2764
- try {
2765
- message = Pusher.Protocol.decodeMessage(m);
2766
- } catch(e) {
2767
- self.emit('error', {
2768
- type: 'MessageParseError',
2769
- error: e,
2770
- data: m.data
2771
- });
2772
- }
3020
+ var listeners = {
3021
+ message: function(m) {
3022
+ var message;
3023
+ try {
3024
+ message = Pusher.Protocol.decodeMessage(m);
3025
+ } catch(e) {
3026
+ self.emit('error', {
3027
+ type: 'MessageParseError',
3028
+ error: e,
3029
+ data: m.data
3030
+ });
3031
+ }
2773
3032
 
2774
- if (message !== undefined) {
2775
- Pusher.debug('Event recd', message);
3033
+ if (message !== undefined) {
3034
+ Pusher.debug('Event recd', message);
3035
+
3036
+ switch (message.event) {
3037
+ case 'pusher:error':
3038
+ self.emit('error', { type: 'PusherError', data: message.data });
3039
+ break;
3040
+ case 'pusher:ping':
3041
+ self.emit("ping");
3042
+ break;
3043
+ case 'pusher:pong':
3044
+ self.emit("pong");
3045
+ break;
3046
+ }
3047
+ self.emit('message', message);
3048
+ }
3049
+ },
3050
+ activity: function() {
3051
+ self.emit("activity");
3052
+ },
3053
+ error: function(error) {
3054
+ self.emit("error", { type: "WebSocketError", error: error });
3055
+ },
3056
+ closed: function(closeEvent) {
3057
+ unbindListeners();
2776
3058
 
2777
- switch (message.event) {
2778
- case 'pusher:error':
2779
- self.emit('error', { type: 'PusherError', data: message.data });
2780
- break;
2781
- case 'pusher:ping':
2782
- self.emit("ping");
2783
- break;
2784
- case 'pusher:pong':
2785
- self.emit("pong");
2786
- break;
3059
+ if (closeEvent && closeEvent.code) {
3060
+ self.handleCloseEvent(closeEvent);
2787
3061
  }
2788
- self.emit('message', message);
2789
- }
2790
- };
2791
- var onError = function(error) {
2792
- self.emit("error", { type: "WebSocketError", error: error });
2793
- };
2794
- var onClosed = function(closeEvent) {
2795
- unbindListeners();
2796
3062
 
2797
- if (closeEvent && closeEvent.code) {
2798
- self.handleCloseEvent(closeEvent);
3063
+ self.transport = null;
3064
+ self.emit("closed");
2799
3065
  }
2800
-
2801
- self.transport = null;
2802
- self.emit("closed");
2803
3066
  };
2804
3067
 
2805
3068
  var unbindListeners = function() {
2806
- self.transport.unbind("closed", onClosed);
2807
- self.transport.unbind("error", onError);
2808
- self.transport.unbind("message", onMessage);
3069
+ Pusher.Util.objectApply(listeners, function(listener, event) {
3070
+ self.transport.unbind(event, listener);
3071
+ });
2809
3072
  };
2810
3073
 
2811
- self.transport.bind("message", onMessage);
2812
- self.transport.bind("error", onError);
2813
- self.transport.bind("closed", onClosed);
3074
+ Pusher.Util.objectApply(listeners, function(listener, event) {
3075
+ self.transport.bind(event, listener);
3076
+ });
2814
3077
  };
2815
3078
 
2816
3079
  /** @private */
@@ -2960,7 +3223,7 @@
2960
3223
  });
2961
3224
  Pusher.Network.bind("offline", function() {
2962
3225
  self.timeline.info({ netinfo: "offline" });
2963
- if (self.state === "connected") {
3226
+ if (self.connection) {
2964
3227
  self.sendActivityCheck();
2965
3228
  }
2966
3229
  });
@@ -3058,7 +3321,6 @@
3058
3321
  this.abortConnecting();
3059
3322
  this.clearRetryTimer();
3060
3323
  this.clearUnavailableTimer();
3061
- this.stopActivityCheck();
3062
3324
  if (this.connection) {
3063
3325
  var connection = this.abandonConnection();
3064
3326
  connection.close();
@@ -3117,7 +3379,7 @@
3117
3379
  prototype.sendActivityCheck = function() {
3118
3380
  var self = this;
3119
3381
  self.stopActivityCheck();
3120
- self.send_event('pusher:ping', {});
3382
+ self.connection.ping();
3121
3383
  // wait for pong response
3122
3384
  self.activityTimer = new Pusher.Timer(
3123
3385
  self.options.pongTimeout,
@@ -3133,7 +3395,7 @@
3133
3395
  var self = this;
3134
3396
  self.stopActivityCheck();
3135
3397
  // send ping after inactivity
3136
- if (!self.connection.supportsPing()) {
3398
+ if (!self.connection.handlesActivityChecks()) {
3137
3399
  self.activityTimer = new Pusher.Timer(self.activityTimeout, function() {
3138
3400
  self.sendActivityCheck();
3139
3401
  });
@@ -3159,6 +3421,9 @@
3159
3421
  ping: function() {
3160
3422
  self.send_event('pusher:pong', {});
3161
3423
  },
3424
+ activity: function() {
3425
+ self.resetActivityCheck();
3426
+ },
3162
3427
  error: function(error) {
3163
3428
  // just emit error to user - socket will already be closed by browser
3164
3429
  self.emit("error", { type: "WebSocketError", error: error });
@@ -3235,6 +3500,7 @@
3235
3500
  if (!this.connection) {
3236
3501
  return;
3237
3502
  }
3503
+ this.stopActivityCheck();
3238
3504
  for (var event in this.connectionCallbacks) {
3239
3505
  this.connection.unbind(event, this.connectionCallbacks[event]);
3240
3506
  }
@@ -3689,8 +3955,8 @@
3689
3955
  }
3690
3956
 
3691
3957
  xhr.onreadystatechange = function() {
3692
- if (xhr.readyState == 4) {
3693
- if (xhr.status == 200) {
3958
+ if (xhr.readyState === 4) {
3959
+ if (xhr.status === 200) {
3694
3960
  var data, parsed = false;
3695
3961
 
3696
3962
  try {