pusher-fake 0.14.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 {