pusher_rails 0.3.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: 3cabf21e41ef487b94bf5fbb948228d57ad343b9
4
- data.tar.gz: 01a24e6d002d6d2a22882f9e63bb73c51bccda79
3
+ metadata.gz: 3179ba51d8e8259fdcbd446b60e6e5c87a18886e
4
+ data.tar.gz: 2a672bd542b91f62a5ab8c26aba5ad1650bf5ea7
5
5
  SHA512:
6
- metadata.gz: 1020d025a5b2491b67595a4cc7646789984b48c9e4d840da8be9803db4b51c3920c9e3e5ffc2630e1726f3aae241cee5e7e27477254da73f1bd3b7aad2d1e9ea
7
- data.tar.gz: 9884af8e5537f39f2fdecaaf5c71a69035ccd24cfc2efe1c49eac11fa01a78638c3f442d9cc124b7838e33a7d9cee24971fa6b9e59bb988d325f508c89142674
6
+ metadata.gz: 7eec94660281c2fa773cc2a5d6d0fa4cd5dfbfb73d98abfd6703de63b0ca6b81d568b2b340c3194421e80b255a77b9d6d4c5de95443be85b3cf5efee01f27209
7
+ data.tar.gz: 13b1878f0357078485f6d3e8108d9f77e0c103ab3c1f39a108083973a17bca67f0c7a9fbcc0f903ad00d66cb84a8ff01fbbd054ac4958a7c17751f265b573adb
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  *.gem
2
+ .rvmrc
@@ -1,3 +1,16 @@
1
+ v0.2.5
2
+ ======
3
+ - Upgraded to pusher-gem 0.9.4
4
+
5
+ v0.2.4
6
+ ======
7
+ - Upgraded to pusher.js 1.12.2
8
+
9
+ v0.2.3
10
+ ======
11
+ - Upgraded to pusher.js 1.12.1
12
+ - Upgraded to pusher-gem 0.9.3
13
+
1
14
  v0.2.2
2
15
  ======
3
16
  - Upgraded to pusher.js 1.12.0
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  =====================
3
3
 
4
4
  Adds:
5
- - [pusher-gem v0.9.2](https://github.com/pusher/pusher-gem/tree/v0.9.2)
6
- - [pusher.js v1.12.0](https://github.com/pusher/pusher-js/tree/v1.12.0)
5
+ - [pusher-gem v0.9.4](https://github.com/pusher/pusher-gem/tree/v0.9.4)
6
+ - [pusher.js v1.12.2](https://github.com/pusher/pusher-js/tree/v1.12.2)
7
7
  - [backpusher.js](https://github.com/pusher/backpusher)
8
8
 
9
9
  This pulls in the *pusher-gem* as well as adding *pusher.js* and *backpusher.js* to the assets pipeline of your Rails 3.1+ app.
@@ -21,7 +21,7 @@ Licenses
21
21
  ========
22
22
 
23
23
  /*!
24
- * Pusher JavaScript Library v1.12.0
24
+ * Pusher JavaScript Library v1.12.2
25
25
  * http://pusherapp.com/
26
26
  *
27
27
  * Copyright 2011, Pusher
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'pusher_rails'
6
- s.version = '0.3.0'
6
+ s.version = '1.0.0'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["David Grandinetti"]
9
9
  s.email = ["dave@wegoto12.com"]
@@ -1,924 +1,2946 @@
1
1
  /*!
2
- * Pusher JavaScript Library v1.12.7
2
+ * Pusher JavaScript Library v2.0.0
3
3
  * http://pusherapp.com/
4
4
  *
5
- * Copyright 2011, Pusher
5
+ * Copyright 2013, Pusher
6
6
  * Released under the MIT licence.
7
7
  */
8
8
 
9
9
  ;(function() {
10
- if (Function.prototype.scopedTo === undefined) {
11
- Function.prototype.scopedTo = function(context, args) {
12
- var f = this;
13
- return function() {
14
- return f.apply(context, Array.prototype.slice.call(args || [])
15
- .concat(Array.prototype.slice.call(arguments)));
16
- };
17
- };
18
- }
10
+ function Pusher(app_key, options) {
11
+ var self = this;
19
12
 
20
- var Pusher = function(app_key, options) {
21
13
  this.options = options || {};
22
14
  this.key = app_key;
23
15
  this.channels = new Pusher.Channels();
24
- this.global_emitter = new Pusher.EventsDispatcher()
16
+ this.global_emitter = new Pusher.EventsDispatcher();
17
+ this.sessionID = Math.floor(Math.random() * 1000000000);
25
18
 
26
- var self = this;
19
+ checkAppKey(this.key);
20
+
21
+ var getStrategy = function(options) {
22
+ return Pusher.StrategyBuilder.build(
23
+ Pusher.getDefaultStrategy(),
24
+ Pusher.Util.extend({}, self.options, options)
25
+ );
26
+ };
27
+ var getTimeline = function() {
28
+ return new Pusher.Timeline(self.key, self.sessionID, {
29
+ features: Pusher.Util.getClientFeatures(),
30
+ params: self.options.timelineParams || {},
31
+ limit: 25,
32
+ level: Pusher.Timeline.INFO,
33
+ version: Pusher.VERSION
34
+ });
35
+ };
36
+ var getTimelineSender = function(timeline, options) {
37
+ if (self.options.disableStats) {
38
+ return null;
39
+ }
40
+ return new Pusher.TimelineSender(timeline, {
41
+ encrypted: self.isEncrypted() || !!options.encrypted,
42
+ host: Pusher.stats_host,
43
+ path: "/timeline"
44
+ });
45
+ };
46
+
47
+ this.connection = new Pusher.ConnectionManager(
48
+ this.key,
49
+ Pusher.Util.extend(
50
+ { getStrategy: getStrategy,
51
+ getTimeline: getTimeline,
52
+ getTimelineSender: getTimelineSender,
53
+ activityTimeout: Pusher.activity_timeout,
54
+ pongTimeout: Pusher.pong_timeout,
55
+ unavailableTimeout: Pusher.unavailable_timeout
56
+ },
57
+ this.options,
58
+ { encrypted: this.isEncrypted() }
59
+ )
60
+ );
61
+
62
+ this.connection.bind('connected', function() {
63
+ self.subscribeAll();
64
+ });
65
+ this.connection.bind('message', function(params) {
66
+ var internal = (params.event.indexOf('pusher_internal:') === 0);
67
+ if (params.channel) {
68
+ var channel = self.channel(params.channel);
69
+ if (channel) {
70
+ channel.emit(params.event, params.data);
71
+ }
72
+ }
73
+ // Emit globaly [deprecated]
74
+ if (!internal) self.global_emitter.emit(params.event, params.data);
75
+ });
76
+ this.connection.bind('disconnected', function() {
77
+ self.channels.disconnect();
78
+ });
79
+ this.connection.bind('error', function(err) {
80
+ Pusher.warn('Error', err);
81
+ });
82
+
83
+ Pusher.instances.push(this);
27
84
 
28
- this.checkAppKey();
85
+ if (Pusher.isReady) self.connect();
86
+ }
87
+ var prototype = Pusher.prototype;
29
88
 
30
- this.connection = new Pusher.Connection(this.key, this.options);
89
+ Pusher.instances = [];
90
+ Pusher.isReady = false;
31
91
 
32
- // Setup / teardown connection
33
- this.connection
34
- .bind('connected', function() {
35
- self.subscribeAll();
36
- })
37
- .bind('message', function(params) {
38
- var internal = (params.event.indexOf('pusher_internal:') === 0);
39
- if (params.channel) {
40
- var channel;
41
- if (channel = self.channel(params.channel)) {
42
- channel.emit(params.event, params.data);
92
+ // To receive log output provide a Pusher.log function, for example
93
+ // Pusher.log = function(m){console.log(m)}
94
+ Pusher.debug = function() {
95
+ if (!Pusher.log) {
96
+ return;
97
+ }
98
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments));
99
+ };
100
+
101
+ Pusher.warn = function() {
102
+ if (window.console && window.console.warn) {
103
+ window.console.warn(Pusher.Util.stringify.apply(this, arguments));
104
+ } else {
105
+ if (!Pusher.log) {
106
+ return;
107
+ }
108
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments));
109
+ }
110
+ };
111
+
112
+ Pusher.ready = function() {
113
+ Pusher.isReady = true;
114
+ for (var i = 0, l = Pusher.instances.length; i < l; i++) {
115
+ Pusher.instances[i].connect();
116
+ }
117
+ };
118
+
119
+ prototype.channel = function(name) {
120
+ return this.channels.find(name);
121
+ };
122
+
123
+ prototype.connect = function() {
124
+ this.connection.connect();
125
+ };
126
+
127
+ prototype.disconnect = function() {
128
+ this.connection.disconnect();
129
+ };
130
+
131
+ prototype.bind = function(event_name, callback) {
132
+ this.global_emitter.bind(event_name, callback);
133
+ return this;
134
+ };
135
+
136
+ prototype.bind_all = function(callback) {
137
+ this.global_emitter.bind_all(callback);
138
+ return this;
139
+ };
140
+
141
+ prototype.subscribeAll = function() {
142
+ var channelName;
143
+ for (channelName in this.channels.channels) {
144
+ if (this.channels.channels.hasOwnProperty(channelName)) {
145
+ this.subscribe(channelName);
146
+ }
147
+ }
148
+ };
149
+
150
+ prototype.subscribe = function(channel_name) {
151
+ var self = this;
152
+ var channel = this.channels.add(channel_name, this);
153
+
154
+ if (this.connection.state === 'connected') {
155
+ channel.authorize(
156
+ this.connection.socket_id,
157
+ this.options,
158
+ function(err, data) {
159
+ if (err) {
160
+ channel.emit('pusher:subscription_error', data);
161
+ } else {
162
+ self.send_event('pusher:subscribe', {
163
+ channel: channel_name,
164
+ auth: data.auth,
165
+ channel_data: data.channel_data
166
+ });
43
167
  }
44
168
  }
45
- // Emit globaly [deprecated]
46
- if (!internal) self.global_emitter.emit(params.event, params.data);
47
- })
48
- .bind('disconnected', function() {
49
- self.channels.disconnect();
50
- })
51
- .bind('error', function(err) {
52
- Pusher.warn('Error', err);
169
+ );
170
+ }
171
+ return channel;
172
+ };
173
+
174
+ prototype.unsubscribe = function(channel_name) {
175
+ this.channels.remove(channel_name);
176
+ if (this.connection.state === 'connected') {
177
+ this.send_event('pusher:unsubscribe', {
178
+ channel: channel_name
53
179
  });
180
+ }
181
+ };
54
182
 
55
- Pusher.instances.push(this);
183
+ prototype.send_event = function(event_name, data, channel) {
184
+ return this.connection.send_event(event_name, data, channel);
185
+ };
56
186
 
57
- if (Pusher.isReady) self.connect();
187
+ prototype.isEncrypted = function() {
188
+ if (Pusher.Util.getDocumentLocation().protocol === "https:") {
189
+ return true;
190
+ } else {
191
+ return !!this.options.encrypted;
192
+ }
58
193
  };
59
- Pusher.instances = [];
60
- Pusher.prototype = {
61
- channel: function(name) {
62
- return this.channels.find(name);
194
+
195
+ function checkAppKey(key) {
196
+ if (key === null || key === undefined) {
197
+ Pusher.warn(
198
+ 'Warning', 'You must pass your app key when you instantiate Pusher.'
199
+ );
200
+ }
201
+ }
202
+
203
+ this.Pusher = Pusher;
204
+ }).call(this);
205
+
206
+ ;(function() {
207
+ Pusher.Util = {
208
+ now: function() {
209
+ if (Date.now) {
210
+ return Date.now();
211
+ } else {
212
+ return new Date().valueOf();
213
+ }
63
214
  },
64
215
 
65
- connect: function() {
66
- this.connection.connect();
216
+ /** Merges multiple objects into the target argument.
217
+ *
218
+ * For properties that are plain Objects, performs a deep-merge. For the
219
+ * rest it just copies the value of the property.
220
+ *
221
+ * To extend prototypes use it as following:
222
+ * Pusher.Util.extend(Target.prototype, Base.prototype)
223
+ *
224
+ * You can also use it to merge objects without altering them:
225
+ * Pusher.Util.extend({}, object1, object2)
226
+ *
227
+ * @param {Object} target
228
+ * @return {Object} the target argument
229
+ */
230
+ extend: function(target) {
231
+ for (var i = 1; i < arguments.length; i++) {
232
+ var extensions = arguments[i];
233
+ for (var property in extensions) {
234
+ if (extensions[property] && extensions[property].constructor &&
235
+ extensions[property].constructor === Object) {
236
+ target[property] = Pusher.Util.extend(
237
+ target[property] || {}, extensions[property]
238
+ );
239
+ } else {
240
+ target[property] = extensions[property];
241
+ }
242
+ }
243
+ }
244
+ return target;
67
245
  },
68
246
 
69
- disconnect: function() {
70
- this.connection.disconnect();
247
+ stringify: function() {
248
+ var m = ["Pusher"];
249
+ for (var i = 0; i < arguments.length; i++) {
250
+ if (typeof arguments[i] === "string") {
251
+ m.push(arguments[i]);
252
+ } else {
253
+ if (window.JSON === undefined) {
254
+ m.push(arguments[i].toString());
255
+ } else {
256
+ m.push(JSON.stringify(arguments[i]));
257
+ }
258
+ }
259
+ }
260
+ return m.join(" : ");
261
+ },
262
+
263
+ arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf
264
+ var nativeIndexOf = Array.prototype.indexOf;
265
+ if (array === null) {
266
+ return -1;
267
+ }
268
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) {
269
+ return array.indexOf(item);
270
+ }
271
+ for (var i = 0, l = array.length; i < l; i++) {
272
+ if (array[i] === item) {
273
+ return i;
274
+ }
275
+ }
276
+ return -1;
71
277
  },
72
278
 
73
- bind: function(event_name, callback) {
74
- this.global_emitter.bind(event_name, callback);
75
- return this;
279
+ keys: function(object) {
280
+ var result = [];
281
+ for (var key in object) {
282
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
283
+ result.push(key);
284
+ }
285
+ }
286
+ return result;
287
+ },
288
+
289
+ /** Applies a function f to all elements of an array.
290
+ *
291
+ * Function f gets 3 arguments passed:
292
+ * - element from the array
293
+ * - index of the element
294
+ * - reference to the array
295
+ *
296
+ * @param {Array} array
297
+ * @param {Function} f
298
+ */
299
+ apply: function(array, f) {
300
+ for (var i = 0; i < array.length; i++) {
301
+ f(array[i], i, array);
302
+ }
303
+ },
304
+
305
+ /** Applies a function f to all properties of an object.
306
+ *
307
+ * Function f gets 3 arguments passed:
308
+ * - element from the object
309
+ * - key of the element
310
+ * - reference to the object
311
+ *
312
+ * @param {Object} object
313
+ * @param {Function} f
314
+ */
315
+ objectApply: function(object, f) {
316
+ for (var key in object) {
317
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
318
+ f(object[key], key, object);
319
+ }
320
+ }
321
+ },
322
+
323
+ /** Maps all elements of the array and returns the result.
324
+ *
325
+ * Function f gets 4 arguments passed:
326
+ * - element from the array
327
+ * - index of the element
328
+ * - reference to the source array
329
+ * - reference to the destination array
330
+ *
331
+ * @param {Array} array
332
+ * @param {Function} f
333
+ */
334
+ map: function(array, f) {
335
+ var result = [];
336
+ for (var i = 0; i < array.length; i++) {
337
+ result.push(f(array[i], i, array, result));
338
+ }
339
+ return result;
340
+ },
341
+
342
+ /** Maps all elements of the object and returns the result.
343
+ *
344
+ * Function f gets 4 arguments passed:
345
+ * - element from the object
346
+ * - key of the element
347
+ * - reference to the source object
348
+ * - reference to the destination object
349
+ *
350
+ * @param {Object} object
351
+ * @param {Function} f
352
+ */
353
+ mapObject: function(object, f) {
354
+ var result = {};
355
+ for (var key in object) {
356
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
357
+ result[key] = f(object[key]);
358
+ }
359
+ }
360
+ return result;
361
+ },
362
+
363
+ /** Filters elements of the array using a test function.
364
+ *
365
+ * Function test gets 4 arguments passed:
366
+ * - element from the array
367
+ * - index of the element
368
+ * - reference to the source array
369
+ * - reference to the destination array
370
+ *
371
+ * @param {Array} array
372
+ * @param {Function} f
373
+ */
374
+ filter: function(array, test) {
375
+ test = test || function(value) { return !!value; };
376
+
377
+ var result = [];
378
+ for (var i = 0; i < array.length; i++) {
379
+ if (test(array[i], i, array, result)) {
380
+ result.push(array[i]);
381
+ }
382
+ }
383
+ return result;
384
+ },
385
+
386
+ /** Filters properties of the object using a test function.
387
+ *
388
+ * Function test gets 4 arguments passed:
389
+ * - element from the object
390
+ * - key of the element
391
+ * - reference to the source object
392
+ * - reference to the destination object
393
+ *
394
+ * @param {Object} object
395
+ * @param {Function} f
396
+ */
397
+ filterObject: function(object, test) {
398
+ test = test || function(value) { return !!value; };
399
+
400
+ var result = {};
401
+ for (var key in object) {
402
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
403
+ if (test(object[key], key, object, result)) {
404
+ result[key] = object[key];
405
+ }
406
+ }
407
+ }
408
+ return result;
409
+ },
410
+
411
+ /** Flattens an object into a two-dimensional array.
412
+ *
413
+ * @param {Object} object
414
+ * @return {Array} resulting array of [key, value] pairs
415
+ */
416
+ flatten: function(object) {
417
+ var result = [];
418
+ for (var key in object) {
419
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
420
+ result.push([key, object[key]]);
421
+ }
422
+ }
423
+ return result;
424
+ },
425
+
426
+ /** Checks whether any element of the array passes the test.
427
+ *
428
+ * Function test gets 3 arguments passed:
429
+ * - element from the array
430
+ * - index of the element
431
+ * - reference to the source array
432
+ *
433
+ * @param {Array} array
434
+ * @param {Function} f
435
+ */
436
+ any: function(array, test) {
437
+ for (var i = 0; i < array.length; i++) {
438
+ if (test(array[i], i, array)) {
439
+ return true;
440
+ }
441
+ }
442
+ return false;
443
+ },
444
+
445
+ /** Checks whether all elements of the array pass the test.
446
+ *
447
+ * Function test gets 3 arguments passed:
448
+ * - element from the array
449
+ * - index of the element
450
+ * - reference to the source array
451
+ *
452
+ * @param {Array} array
453
+ * @param {Function} f
454
+ */
455
+ all: function(array, test) {
456
+ for (var i = 0; i < array.length; i++) {
457
+ if (!test(array[i], i, array)) {
458
+ return false;
459
+ }
460
+ }
461
+ return true;
462
+ },
463
+
464
+ /** Builds a function that will proxy a method call to its first argument.
465
+ *
466
+ * Allows partial application of arguments, so additional arguments are
467
+ * prepended to the argument list.
468
+ *
469
+ * @param {String} name method name
470
+ * @return {Function} proxy function
471
+ */
472
+ method: function(name) {
473
+ var boundArguments = Array.prototype.slice.call(arguments, 1);
474
+ return function(object) {
475
+ return object[name].apply(object, boundArguments.concat(arguments));
476
+ };
477
+ },
478
+
479
+ getDocument: function() {
480
+ return document;
481
+ },
482
+
483
+ getDocumentLocation: function() {
484
+ return Pusher.Util.getDocument().location;
76
485
  },
77
486
 
78
- bind_all: function(callback) {
79
- this.global_emitter.bind_all(callback);
80
- return this;
487
+ getLocalStorage: function() {
488
+ return window.localStorage;
81
489
  },
82
490
 
83
- subscribeAll: function() {
84
- var channelName;
85
- for (channelName in this.channels.channels) {
86
- if (this.channels.channels.hasOwnProperty(channelName)) {
87
- this.subscribe(channelName);
491
+ getClientFeatures: function() {
492
+ return Pusher.Util.keys(
493
+ Pusher.Util.filterObject(
494
+ { "ws": Pusher.WSTransport, "flash": Pusher.FlashTransport },
495
+ function (t) { return t.isSupported(); }
496
+ )
497
+ );
498
+ }
499
+ };
500
+ }).call(this);
501
+
502
+ ;(function() {
503
+ Pusher.VERSION = '2.0.0';
504
+ Pusher.PROTOCOL = 6;
505
+
506
+ // WS connection parameters
507
+ Pusher.host = 'ws.pusherapp.com';
508
+ Pusher.ws_port = 80;
509
+ Pusher.wss_port = 443;
510
+ // SockJS fallback parameters
511
+ Pusher.sockjs_host = 'sockjs.pusher.com';
512
+ Pusher.sockjs_http_port = 80;
513
+ Pusher.sockjs_https_port = 443;
514
+ Pusher.sockjs_path = "/pusher";
515
+ // Stats
516
+ Pusher.stats_host = 'stats.pusher.com';
517
+ // Other settings
518
+ Pusher.channel_auth_endpoint = '/pusher/auth';
519
+ Pusher.cdn_http = 'http://js.pusher.com/';
520
+ Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/';
521
+ Pusher.dependency_suffix = '';
522
+ Pusher.channel_auth_transport = 'ajax';
523
+ Pusher.activity_timeout = 120000;
524
+ Pusher.pong_timeout = 30000;
525
+ Pusher.unavailable_timeout = 10000;
526
+
527
+ Pusher.getDefaultStrategy = function() {
528
+ return [
529
+ [":def", "ws_options", {
530
+ hostUnencrypted: Pusher.host + ":" + Pusher.ws_port,
531
+ hostEncrypted: Pusher.host + ":" + Pusher.wss_port
532
+ }],
533
+ [":def", "sockjs_options", {
534
+ hostUnencrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_http_port,
535
+ hostEncrypted: Pusher.sockjs_host + ":" + Pusher.sockjs_https_port
536
+ }],
537
+ [":def", "timeouts", {
538
+ loop: true,
539
+ timeout: 15000,
540
+ timeoutLimit: 60000
541
+ }],
542
+
543
+ [":def", "ws_manager", [":transport_manager", { lives: 2 }]],
544
+ [":def_transport", "ws", "ws", 3, ":ws_options", ":ws_manager"],
545
+ [":def_transport", "flash", "flash", 2, ":ws_options", ":ws_manager"],
546
+ [":def_transport", "sockjs", "sockjs", 1, ":sockjs_options"],
547
+ [":def", "ws_loop", [":sequential", ":timeouts", ":ws"]],
548
+ [":def", "flash_loop", [":sequential", ":timeouts", ":flash"]],
549
+ [":def", "sockjs_loop", [":sequential", ":timeouts", ":sockjs"]],
550
+
551
+ [":def", "strategy",
552
+ [":cached", 1800000,
553
+ [":first_connected",
554
+ [":if", [":is_supported", ":ws"], [
555
+ ":best_connected_ever", ":ws_loop", [":delayed", 2000, [":sockjs_loop"]]
556
+ ], [":if", [":is_supported", ":flash"], [
557
+ ":best_connected_ever", ":flash_loop", [":delayed", 2000, [":sockjs_loop"]]
558
+ ], [
559
+ ":sockjs_loop"
560
+ ]
561
+ ]]
562
+ ]
563
+ ]
564
+ ]
565
+ ];
566
+ };
567
+ }).call(this);
568
+
569
+ ;(function() {
570
+ function buildExceptionClass(name) {
571
+ var klass = function(message) {
572
+ Error.call(this, message);
573
+ this.name = name;
574
+ };
575
+ Pusher.Util.extend(klass.prototype, Error.prototype);
576
+
577
+ return klass;
578
+ }
579
+
580
+ /** Error classes used throughout pusher-js library. */
581
+ Pusher.Errors = {
582
+ UnsupportedTransport: buildExceptionClass("UnsupportedTransport"),
583
+ UnsupportedStrategy: buildExceptionClass("UnsupportedStrategy"),
584
+ TransportPriorityTooLow: buildExceptionClass("TransportPriorityTooLow"),
585
+ TransportClosed: buildExceptionClass("TransportClosed")
586
+ };
587
+ }).call(this);
588
+
589
+ ;(function() {
590
+ /** Manages callback bindings and event emitting.
591
+ *
592
+ * @param Function failThrough called when no listeners are bound to an event
593
+ */
594
+ function EventsDispatcher(failThrough) {
595
+ this.callbacks = new CallbackRegistry();
596
+ this.global_callbacks = [];
597
+ this.failThrough = failThrough;
598
+ }
599
+ var prototype = EventsDispatcher.prototype;
600
+
601
+ prototype.bind = function(eventName, callback) {
602
+ this.callbacks.add(eventName, callback);
603
+ return this;
604
+ };
605
+
606
+ prototype.bind_all = function(callback) {
607
+ this.global_callbacks.push(callback);
608
+ return this;
609
+ };
610
+
611
+ prototype.unbind = function(eventName, callback) {
612
+ this.callbacks.remove(eventName, callback);
613
+ return this;
614
+ };
615
+
616
+ prototype.emit = function(eventName, data) {
617
+ var i;
618
+
619
+ for (i = 0; i < this.global_callbacks.length; i++) {
620
+ this.global_callbacks[i](eventName, data);
621
+ }
622
+
623
+ var callbacks = this.callbacks.get(eventName);
624
+ if (callbacks && callbacks.length > 0) {
625
+ for (i = 0; i < callbacks.length; i++) {
626
+ callbacks[i](data);
627
+ }
628
+ } else if (this.failThrough) {
629
+ this.failThrough(eventName, data);
630
+ }
631
+
632
+ return this;
633
+ };
634
+
635
+ /** Callback registry helper. */
636
+
637
+ function CallbackRegistry() {
638
+ this._callbacks = {};
639
+ }
640
+
641
+ CallbackRegistry.prototype.get = function(eventName) {
642
+ return this._callbacks[this._prefix(eventName)];
643
+ };
644
+
645
+ CallbackRegistry.prototype.add = function(eventName, callback) {
646
+ var prefixedEventName = this._prefix(eventName);
647
+ this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || [];
648
+ this._callbacks[prefixedEventName].push(callback);
649
+ };
650
+
651
+ CallbackRegistry.prototype.remove = function(eventName, callback) {
652
+ if(this.get(eventName)) {
653
+ var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback);
654
+ var callbacksCopy = this._callbacks[this._prefix(eventName)].slice(0);
655
+ callbacksCopy.splice(index, 1);
656
+ this._callbacks[this._prefix(eventName)] = callbacksCopy;
657
+ }
658
+ };
659
+
660
+ CallbackRegistry.prototype._prefix = function(eventName) {
661
+ return "_" + eventName;
662
+ };
663
+
664
+ Pusher.EventsDispatcher = EventsDispatcher;
665
+ }).call(this);
666
+
667
+ ;(function() {
668
+ /** Handles loading dependency files.
669
+ *
670
+ * Options:
671
+ * - cdn_http - url to HTTP CND
672
+ * - cdn_https - url to HTTPS CDN
673
+ * - version - version of pusher-js
674
+ * - suffix - suffix appended to all names of dependency files
675
+ *
676
+ * @param {Object} options
677
+ */
678
+ function DependencyLoader(options) {
679
+ this.options = options;
680
+ this.loading = {};
681
+ this.loaded = {};
682
+ }
683
+ var prototype = DependencyLoader.prototype;
684
+
685
+ /** Loads the dependency from CDN.
686
+ *
687
+ * @param {String} name
688
+ * @param {Function} callback
689
+ */
690
+ prototype.load = function(name, callback) {
691
+ var self = this;
692
+
693
+ if (this.loaded[name]) {
694
+ callback();
695
+ return;
696
+ }
697
+
698
+ if (!this.loading[name]) {
699
+ this.loading[name] = [];
700
+ }
701
+ this.loading[name].push(callback);
702
+ if (this.loading[name].length > 1) {
703
+ return;
704
+ }
705
+
706
+ require(this.getPath(name), function() {
707
+ for (var i = 0; i < self.loading[name].length; i++) {
708
+ self.loading[name][i]();
709
+ }
710
+ delete self.loading[name];
711
+ self.loaded[name] = true;
712
+ });
713
+ };
714
+
715
+ /** Returns a root URL for pusher-js CDN.
716
+ *
717
+ * @returns {String}
718
+ */
719
+ prototype.getRoot = function(options) {
720
+ var cdn;
721
+ var protocol = Pusher.Util.getDocumentLocation().protocol;
722
+ if ((options && options.encrypted) || protocol === "https:") {
723
+ cdn = this.options.cdn_https;
724
+ } else {
725
+ cdn = this.options.cdn_http;
726
+ }
727
+ // make sure there are no double slashes
728
+ return cdn.replace(/\/*$/, "") + "/" + this.options.version;
729
+ };
730
+
731
+ /** Returns a full path to a dependency file.
732
+ *
733
+ * @param {String} name
734
+ * @returns {String}
735
+ */
736
+ prototype.getPath = function(name, options) {
737
+ return this.getRoot(options) + '/' + name + this.options.suffix + '.js';
738
+ };
739
+
740
+ function handleScriptLoaded(elem, callback) {
741
+ if (Pusher.Util.getDocument().addEventListener) {
742
+ elem.addEventListener('load', callback, false);
743
+ } else {
744
+ elem.attachEvent('onreadystatechange', function () {
745
+ if (elem.readyState === 'loaded' || elem.readyState === 'complete') {
746
+ callback();
747
+ }
748
+ });
749
+ }
750
+ }
751
+
752
+ function require(src, callback) {
753
+ var document = Pusher.Util.getDocument();
754
+ var head = document.getElementsByTagName('head')[0];
755
+ var script = document.createElement('script');
756
+
757
+ script.setAttribute('src', src);
758
+ script.setAttribute("type","text/javascript");
759
+ script.setAttribute('async', true);
760
+
761
+ handleScriptLoaded(script, function() {
762
+ // workaround for an Opera issue
763
+ setTimeout(callback, 0);
764
+ });
765
+
766
+ head.appendChild(script);
767
+ }
768
+
769
+ Pusher.DependencyLoader = DependencyLoader;
770
+ }).call(this);
771
+
772
+ ;(function() {
773
+ Pusher.Dependencies = new Pusher.DependencyLoader({
774
+ cdn_http: Pusher.cdn_http,
775
+ cdn_https: Pusher.cdn_https,
776
+ version: Pusher.VERSION,
777
+ suffix: Pusher.dependency_suffix
778
+ });
779
+
780
+ // Support Firefox versions which prefix WebSocket
781
+ if (!window.WebSocket && window.MozWebSocket) {
782
+ window.WebSocket = window.MozWebSocket;
783
+ }
784
+
785
+ function initialize() {
786
+ Pusher.ready();
787
+ }
788
+
789
+ // Allows calling a function when the document body is available
790
+ function onDocumentBody(callback) {
791
+ if (document.body) {
792
+ callback();
793
+ } else {
794
+ setTimeout(function() {
795
+ onDocumentBody(callback);
796
+ }, 0);
797
+ }
798
+ }
799
+
800
+ function initializeOnDocumentBody() {
801
+ onDocumentBody(initialize);
802
+ }
803
+
804
+ if (!window.JSON) {
805
+ Pusher.Dependencies.load("json2", initializeOnDocumentBody);
806
+ } else {
807
+ initializeOnDocumentBody();
808
+ }
809
+ })();
810
+
811
+ ;(function() {
812
+ /** Cross-browser compatible timer abstraction.
813
+ *
814
+ * @param {Number} delay
815
+ * @param {Function} callback
816
+ */
817
+ function Timer(delay, callback) {
818
+ var self = this;
819
+
820
+ this.timeout = setTimeout(function() {
821
+ if (self.timeout !== null) {
822
+ callback();
823
+ self.timeout = null;
824
+ }
825
+ }, delay);
826
+ }
827
+ var prototype = Timer.prototype;
828
+
829
+ /** Returns whether the timer is still running.
830
+ *
831
+ * @return {Boolean}
832
+ */
833
+ prototype.isRunning = function() {
834
+ return this.timeout !== null;
835
+ };
836
+
837
+ /** Aborts a timer when it's running. */
838
+ prototype.ensureAborted = function() {
839
+ if (this.timeout) {
840
+ clearTimeout(this.timeout);
841
+ this.timeout = null;
842
+ }
843
+ };
844
+
845
+ Pusher.Timer = Timer;
846
+ }).call(this);
847
+
848
+ (function() {
849
+
850
+ var Base64 = {
851
+ encode: function (s) {
852
+ return btoa(utob(s));
853
+ }
854
+ };
855
+
856
+ var fromCharCode = String.fromCharCode;
857
+
858
+ var b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
859
+ var b64tab = {};
860
+
861
+ for (var i = 0, l = b64chars.length; i < l; i++) {
862
+ b64tab[b64chars.charAt(i)] = i;
863
+ }
864
+
865
+ var cb_utob = function(c) {
866
+ var cc = c.charCodeAt(0);
867
+ return cc < 0x80 ? c
868
+ : cc < 0x800 ? fromCharCode(0xc0 | (cc >>> 6)) +
869
+ fromCharCode(0x80 | (cc & 0x3f))
870
+ : fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
871
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
872
+ fromCharCode(0x80 | ( cc & 0x3f));
873
+ };
874
+
875
+ var utob = function(u) {
876
+ return u.replace(/[^\x00-\x7F]/g, cb_utob);
877
+ };
878
+
879
+ var cb_encode = function(ccc) {
880
+ var padlen = [0, 2, 1][ccc.length % 3];
881
+ var ord = ccc.charCodeAt(0) << 16
882
+ | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
883
+ | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0));
884
+ var chars = [
885
+ b64chars.charAt( ord >>> 18),
886
+ b64chars.charAt((ord >>> 12) & 63),
887
+ padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
888
+ padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
889
+ ];
890
+ return chars.join('');
891
+ };
892
+
893
+ var btoa = window.btoa || function(b) {
894
+ return b.replace(/[\s\S]{1,3}/g, cb_encode);
895
+ };
896
+
897
+ Pusher.Base64 = Base64;
898
+
899
+ }).call(this);
900
+
901
+ (function() {
902
+
903
+ function JSONPRequest(options) {
904
+ this.options = options;
905
+ }
906
+
907
+ JSONPRequest.send = function(options, callback) {
908
+ var request = new Pusher.JSONPRequest({
909
+ url: options.url,
910
+ receiver: options.receiverName,
911
+ tagPrefix: options.tagPrefix
912
+ });
913
+ var id = options.receiver.register(function(error, result) {
914
+ request.cleanup();
915
+ callback(error, result);
916
+ });
917
+
918
+ return request.send(id, options.data, function(error) {
919
+ var callback = options.receiver.unregister(id);
920
+ if (callback) {
921
+ callback(error);
922
+ }
923
+ });
924
+ };
925
+
926
+ var prototype = JSONPRequest.prototype;
927
+
928
+ prototype.send = function(id, data, callback) {
929
+ if (this.script) {
930
+ return false;
931
+ }
932
+
933
+ var tagPrefix = this.options.tagPrefix || "_pusher_jsonp_";
934
+
935
+ var params = Pusher.Util.extend(
936
+ {}, data, { receiver: this.options.receiver }
937
+ );
938
+ var query = Pusher.Util.map(
939
+ Pusher.Util.flatten(
940
+ encodeData(
941
+ Pusher.Util.filterObject(params, function(value) {
942
+ return value !== undefined;
943
+ })
944
+ )
945
+ ),
946
+ Pusher.Util.method("join", "=")
947
+ ).join("&");
948
+
949
+ this.script = document.createElement("script");
950
+ this.script.id = tagPrefix + id;
951
+ this.script.src = this.options.url + "/" + id + "?" + query;
952
+ this.script.type = "text/javascript";
953
+ this.script.charset = "UTF-8";
954
+ this.script.onerror = this.script.onload = callback;
955
+
956
+ // Opera<11.6 hack for missing onerror callback
957
+ if (this.script.async === undefined && document.attachEvent) {
958
+ if (/opera/i.test(navigator.userAgent)) {
959
+ var receiverName = this.options.receiver || "Pusher.JSONP.receive";
960
+ this.errorScript = document.createElement("script");
961
+ this.errorScript.text = receiverName + "(" + id + ", true);";
962
+ this.script.async = this.errorScript.async = false;
963
+ }
964
+ }
965
+
966
+ var self = this;
967
+ this.script.onreadystatechange = function() {
968
+ if (self.script && /loaded|complete/.test(self.script.readyState)) {
969
+ callback(true);
970
+ }
971
+ };
972
+
973
+ var head = document.getElementsByTagName('head')[0];
974
+ head.insertBefore(this.script, head.firstChild);
975
+ if (this.errorScript) {
976
+ head.insertBefore(this.errorScript, this.script.nextSibling);
977
+ }
978
+
979
+ return true;
980
+ };
981
+
982
+ prototype.cleanup = function() {
983
+ if (this.script && this.script.parentNode) {
984
+ this.script.parentNode.removeChild(this.script);
985
+ this.script = null;
986
+ }
987
+ if (this.errorScript && this.errorScript.parentNode) {
988
+ this.errorScript.parentNode.removeChild(this.errorScript);
989
+ this.errorScript = null;
990
+ }
991
+ };
992
+
993
+ function encodeData(data) {
994
+ return Pusher.Util.mapObject(data, function(value) {
995
+ if (typeof value === "object") {
996
+ value = JSON.stringify(value);
997
+ }
998
+ return encodeURIComponent(Pusher.Base64.encode(value.toString()));
999
+ });
1000
+ }
1001
+
1002
+ Pusher.JSONPRequest = JSONPRequest;
1003
+
1004
+ }).call(this);
1005
+
1006
+ (function() {
1007
+
1008
+ function JSONPReceiver() {
1009
+ this.lastId = 0;
1010
+ this.callbacks = {};
1011
+ }
1012
+
1013
+ var prototype = JSONPReceiver.prototype;
1014
+
1015
+ prototype.register = function(callback) {
1016
+ this.lastId++;
1017
+ var id = this.lastId;
1018
+ this.callbacks[id] = callback;
1019
+ return id;
1020
+ };
1021
+
1022
+ prototype.unregister = function(id) {
1023
+ if (this.callbacks[id]) {
1024
+ var callback = this.callbacks[id];
1025
+ delete this.callbacks[id];
1026
+ return callback;
1027
+ } else {
1028
+ return null;
1029
+ }
1030
+ };
1031
+
1032
+ prototype.receive = function(id, error, data) {
1033
+ var callback = this.unregister(id);
1034
+ if (callback) {
1035
+ callback(error, data);
1036
+ }
1037
+ };
1038
+
1039
+ Pusher.JSONPReceiver = JSONPReceiver;
1040
+ Pusher.JSONP = new JSONPReceiver();
1041
+
1042
+ }).call(this);
1043
+
1044
+ (function() {
1045
+ function Timeline(key, session, options) {
1046
+ this.key = key;
1047
+ this.session = session;
1048
+ this.events = [];
1049
+ this.options = options || {};
1050
+ this.sent = 0;
1051
+ this.uniqueID = 0;
1052
+ }
1053
+ var prototype = Timeline.prototype;
1054
+
1055
+ // Log levels
1056
+ Timeline.ERROR = 3;
1057
+ Timeline.INFO = 6;
1058
+ Timeline.DEBUG = 7;
1059
+
1060
+ prototype.log = function(level, event) {
1061
+ if (this.options.level === undefined || level <= this.options.level) {
1062
+ this.events.push(
1063
+ Pusher.Util.extend({}, event, {
1064
+ timestamp: Pusher.Util.now(),
1065
+ level: level
1066
+ })
1067
+ );
1068
+ if (this.options.limit && this.events.length > this.options.limit) {
1069
+ this.events.shift();
1070
+ }
1071
+ }
1072
+ };
1073
+
1074
+ prototype.error = function(event) {
1075
+ this.log(Timeline.ERROR, event);
1076
+ };
1077
+
1078
+ prototype.info = function(event) {
1079
+ this.log(Timeline.INFO, event);
1080
+ };
1081
+
1082
+ prototype.debug = function(event) {
1083
+ this.log(Timeline.DEBUG, event);
1084
+ };
1085
+
1086
+ prototype.isEmpty = function() {
1087
+ return this.events.length === 0;
1088
+ };
1089
+
1090
+ prototype.send = function(sendJSONP, callback) {
1091
+ var self = this;
1092
+
1093
+ var data = {};
1094
+ if (this.sent === 0) {
1095
+ data = Pusher.Util.extend({
1096
+ key: this.key,
1097
+ features: this.options.features,
1098
+ version: this.options.version
1099
+ }, this.options.params || {});
1100
+ }
1101
+ data.session = this.session;
1102
+ data.timeline = this.events;
1103
+ data = Pusher.Util.filterObject(data, function(v) {
1104
+ return v !== undefined;
1105
+ });
1106
+
1107
+ this.events = [];
1108
+ sendJSONP(data, function(error, result) {
1109
+ if (!error) {
1110
+ self.sent++;
1111
+ }
1112
+ callback(error, result);
1113
+ });
1114
+
1115
+ return true;
1116
+ };
1117
+
1118
+ prototype.generateUniqueID = function() {
1119
+ this.uniqueID++;
1120
+ return this.uniqueID;
1121
+ };
1122
+
1123
+ Pusher.Timeline = Timeline;
1124
+ }).call(this);
1125
+
1126
+ (function() {
1127
+ function TimelineSender(timeline, options) {
1128
+ this.timeline = timeline;
1129
+ this.options = options || {};
1130
+ }
1131
+ var prototype = TimelineSender.prototype;
1132
+
1133
+ prototype.send = function(callback) {
1134
+ if (this.timeline.isEmpty()) {
1135
+ return;
1136
+ }
1137
+
1138
+ var options = this.options;
1139
+ var scheme = "http" + (this.isEncrypted() ? "s" : "") + "://";
1140
+
1141
+ var sendJSONP = function(data, callback) {
1142
+ return Pusher.JSONPRequest.send({
1143
+ data: data,
1144
+ url: scheme + options.host + options.path,
1145
+ receiver: Pusher.JSONP
1146
+ }, callback);
1147
+ };
1148
+ this.timeline.send(sendJSONP, callback);
1149
+ };
1150
+
1151
+ prototype.isEncrypted = function() {
1152
+ return !!this.options.encrypted;
1153
+ };
1154
+
1155
+ Pusher.TimelineSender = TimelineSender;
1156
+ }).call(this);
1157
+
1158
+ ;(function() {
1159
+ /** Launches all substrategies and emits prioritized connected transports.
1160
+ *
1161
+ * @param {Array} strategies
1162
+ */
1163
+ function BestConnectedEverStrategy(strategies) {
1164
+ this.strategies = strategies;
1165
+ }
1166
+ var prototype = BestConnectedEverStrategy.prototype;
1167
+
1168
+ prototype.isSupported = function() {
1169
+ return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported"));
1170
+ };
1171
+
1172
+ prototype.connect = function(minPriority, callback) {
1173
+ return connect(this.strategies, minPriority, function(i, runners) {
1174
+ return function(error, connection) {
1175
+ runners[i].error = error;
1176
+ if (error) {
1177
+ if (allRunnersFailed(runners)) {
1178
+ callback(true);
1179
+ }
1180
+ return;
1181
+ }
1182
+ Pusher.Util.apply(runners, function(runner) {
1183
+ runner.forceMinPriority(connection.priority);
1184
+ });
1185
+ callback(null, connection);
1186
+ };
1187
+ });
1188
+ };
1189
+
1190
+ /** Connects to all strategies in parallel.
1191
+ *
1192
+ * Callback builder should be a function that takes two arguments: index
1193
+ * and a list of runners. It should return another function that will be
1194
+ * passed to the substrategy with given index. Runners can be aborted using
1195
+ * abortRunner(s) functions from this class.
1196
+ *
1197
+ * @param {Array} strategies
1198
+ * @param {Function} callbackBuilder
1199
+ * @return {Object} strategy runner
1200
+ */
1201
+ function connect(strategies, minPriority, callbackBuilder) {
1202
+ var runners = Pusher.Util.map(strategies, function(strategy, i, _, rs) {
1203
+ return strategy.connect(minPriority, callbackBuilder(i, rs));
1204
+ });
1205
+ return {
1206
+ abort: function() {
1207
+ Pusher.Util.apply(runners, abortRunner);
1208
+ },
1209
+ forceMinPriority: function(p) {
1210
+ Pusher.Util.apply(runners, function(runner) {
1211
+ runner.forceMinPriority(p);
1212
+ });
1213
+ }
1214
+ };
1215
+ }
1216
+
1217
+ function allRunnersFailed(runners) {
1218
+ return Pusher.Util.all(runners, function(runner) {
1219
+ return Boolean(runner.error);
1220
+ });
1221
+ }
1222
+
1223
+ function abortRunner(runner) {
1224
+ if (!runner.error && !runner.aborted) {
1225
+ runner.abort();
1226
+ runner.aborted = true;
1227
+ }
1228
+ }
1229
+
1230
+ Pusher.BestConnectedEverStrategy = BestConnectedEverStrategy;
1231
+ }).call(this);
1232
+
1233
+ ;(function() {
1234
+ /** Caches last successful transport and uses it for following attempts.
1235
+ *
1236
+ * @param {Strategy} strategy
1237
+ * @param {Object} transports
1238
+ * @param {Object} options
1239
+ */
1240
+ function CachedStrategy(strategy, transports, options) {
1241
+ this.strategy = strategy;
1242
+ this.transports = transports;
1243
+ this.ttl = options.ttl || 1800*1000;
1244
+ this.timeline = options.timeline;
1245
+ }
1246
+ var prototype = CachedStrategy.prototype;
1247
+
1248
+ prototype.isSupported = function() {
1249
+ return this.strategy.isSupported();
1250
+ };
1251
+
1252
+ prototype.connect = function(minPriority, callback) {
1253
+ var info = fetchTransportInfo();
1254
+
1255
+ var strategies = [this.strategy];
1256
+ if (info && info.timestamp + this.ttl >= Pusher.Util.now()) {
1257
+ var transport = this.transports[info.transport];
1258
+ if (transport) {
1259
+ this.timeline.info({ cached: true, transport: info.transport });
1260
+ strategies.push(new Pusher.SequentialStrategy([transport], {
1261
+ timeout: info.latency * 2,
1262
+ failFast: true
1263
+ }));
1264
+ }
1265
+ }
1266
+
1267
+ var startTimestamp = Pusher.Util.now();
1268
+ var runner = strategies.pop().connect(
1269
+ minPriority,
1270
+ function cb(error, connection) {
1271
+ if (error) {
1272
+ flushTransportInfo();
1273
+ if (strategies.length > 0) {
1274
+ startTimestamp = Pusher.Util.now();
1275
+ runner = strategies.pop().connect(minPriority, cb);
1276
+ } else {
1277
+ callback(error);
1278
+ }
1279
+ } else {
1280
+ var latency = Pusher.Util.now() - startTimestamp;
1281
+ storeTransportInfo(connection.name, latency);
1282
+ callback(null, connection);
1283
+ }
1284
+ }
1285
+ );
1286
+
1287
+ return {
1288
+ abort: function() {
1289
+ runner.abort();
1290
+ },
1291
+ forceMinPriority: function(p) {
1292
+ minPriority = p;
1293
+ if (runner) {
1294
+ runner.forceMinPriority(p);
1295
+ }
1296
+ }
1297
+ };
1298
+ };
1299
+
1300
+ function fetchTransportInfo() {
1301
+ var storage = Pusher.Util.getLocalStorage();
1302
+ if (storage) {
1303
+ var info = storage.pusherTransport;
1304
+ if (info) {
1305
+ return JSON.parse(storage.pusherTransport);
1306
+ }
1307
+ }
1308
+ return null;
1309
+ }
1310
+
1311
+ function storeTransportInfo(transport, latency) {
1312
+ var storage = Pusher.Util.getLocalStorage();
1313
+ if (storage) {
1314
+ storage.pusherTransport = JSON.stringify({
1315
+ timestamp: Pusher.Util.now(),
1316
+ transport: transport,
1317
+ latency: latency
1318
+ });
1319
+ }
1320
+ }
1321
+
1322
+ function flushTransportInfo() {
1323
+ var storage = Pusher.Util.getLocalStorage();
1324
+ if (storage) {
1325
+ delete storage.pusherTransport;
1326
+ }
1327
+ }
1328
+
1329
+ Pusher.CachedStrategy = CachedStrategy;
1330
+ }).call(this);
1331
+
1332
+ ;(function() {
1333
+ /** Runs substrategy after specified delay.
1334
+ *
1335
+ * Options:
1336
+ * - delay - time in miliseconds to delay the substrategy attempt
1337
+ *
1338
+ * @param {Strategy} strategy
1339
+ * @param {Object} options
1340
+ */
1341
+ function DelayedStrategy(strategy, options) {
1342
+ this.strategy = strategy;
1343
+ this.options = { delay: options.delay };
1344
+ }
1345
+ var prototype = DelayedStrategy.prototype;
1346
+
1347
+ prototype.isSupported = function() {
1348
+ return this.strategy.isSupported();
1349
+ };
1350
+
1351
+ prototype.connect = function(minPriority, callback) {
1352
+ var strategy = this.strategy;
1353
+ var runner;
1354
+ var timer = new Pusher.Timer(this.options.delay, function() {
1355
+ runner = strategy.connect(minPriority, callback);
1356
+ });
1357
+
1358
+ return {
1359
+ abort: function() {
1360
+ timer.ensureAborted();
1361
+ if (runner) {
1362
+ runner.abort();
1363
+ }
1364
+ },
1365
+ forceMinPriority: function(p) {
1366
+ minPriority = p;
1367
+ if (runner) {
1368
+ runner.forceMinPriority(p);
1369
+ }
1370
+ }
1371
+ };
1372
+ };
1373
+
1374
+ Pusher.DelayedStrategy = DelayedStrategy;
1375
+ }).call(this);
1376
+
1377
+ ;(function() {
1378
+ /** Launches the substrategy and terminates on the first open connection.
1379
+ *
1380
+ * @param {Strategy} strategy
1381
+ */
1382
+ function FirstConnectedStrategy(strategy) {
1383
+ this.strategy = strategy;
1384
+ }
1385
+ var prototype = FirstConnectedStrategy.prototype;
1386
+
1387
+ prototype.isSupported = function() {
1388
+ return this.strategy.isSupported();
1389
+ };
1390
+
1391
+ prototype.connect = function(minPriority, callback) {
1392
+ var runner = this.strategy.connect(
1393
+ minPriority,
1394
+ function(error, connection) {
1395
+ if (connection) {
1396
+ runner.abort();
1397
+ }
1398
+ callback(error, connection);
1399
+ }
1400
+ );
1401
+ return runner;
1402
+ };
1403
+
1404
+ Pusher.FirstConnectedStrategy = FirstConnectedStrategy;
1405
+ }).call(this);
1406
+
1407
+ ;(function() {
1408
+ /** Proxies method calls to one of substrategies basing on the test function.
1409
+ *
1410
+ * @param {Function} test
1411
+ * @param {Strategy} trueBranch strategy used when test returns true
1412
+ * @param {Strategy} falseBranch strategy used when test returns false
1413
+ */
1414
+ function IfStrategy(test, trueBranch, falseBranch) {
1415
+ this.test = test;
1416
+ this.trueBranch = trueBranch;
1417
+ this.falseBranch = falseBranch;
1418
+ }
1419
+ var prototype = IfStrategy.prototype;
1420
+
1421
+ prototype.isSupported = function() {
1422
+ var branch = this.test() ? this.trueBranch : this.falseBranch;
1423
+ return branch.isSupported();
1424
+ };
1425
+
1426
+ prototype.connect = function(minPriority, callback) {
1427
+ var branch = this.test() ? this.trueBranch : this.falseBranch;
1428
+ return branch.connect(minPriority, callback);
1429
+ };
1430
+
1431
+ Pusher.IfStrategy = IfStrategy;
1432
+ }).call(this);
1433
+
1434
+ ;(function() {
1435
+ /** Loops through strategies with optional timeouts.
1436
+ *
1437
+ * Options:
1438
+ * - loop - whether it should loop through the substrategy list
1439
+ * - timeout - initial timeout for a single substrategy
1440
+ * - timeoutLimit - maximum timeout
1441
+ *
1442
+ * @param {Strategy[]} strategies
1443
+ * @param {Object} options
1444
+ */
1445
+ function SequentialStrategy(strategies, options) {
1446
+ this.strategies = strategies;
1447
+ this.loop = Boolean(options.loop);
1448
+ this.failFast = Boolean(options.failFast);
1449
+ this.timeout = options.timeout;
1450
+ this.timeoutLimit = options.timeoutLimit;
1451
+ }
1452
+ var prototype = SequentialStrategy.prototype;
1453
+
1454
+ prototype.isSupported = function() {
1455
+ return Pusher.Util.any(this.strategies, Pusher.Util.method("isSupported"));
1456
+ };
1457
+
1458
+ prototype.connect = function(minPriority, callback) {
1459
+ var self = this;
1460
+
1461
+ var strategies = this.strategies;
1462
+ var current = 0;
1463
+ var timeout = this.timeout;
1464
+ var runner = null;
1465
+
1466
+ var tryNextStrategy = function(error, connection) {
1467
+ if (connection) {
1468
+ callback(null, connection);
1469
+ } else {
1470
+ current = current + 1;
1471
+ if (self.loop) {
1472
+ current = current % strategies.length;
1473
+ }
1474
+
1475
+ if (current < strategies.length) {
1476
+ if (timeout) {
1477
+ timeout = timeout * 2;
1478
+ if (self.timeoutLimit) {
1479
+ timeout = Math.min(timeout, self.timeoutLimit);
1480
+ }
1481
+ }
1482
+ runner = self.tryStrategy(
1483
+ strategies[current],
1484
+ minPriority,
1485
+ { timeout: timeout, failFast: self.failFast },
1486
+ tryNextStrategy
1487
+ );
1488
+ } else {
1489
+ callback(true);
1490
+ }
1491
+ }
1492
+ };
1493
+
1494
+ runner = this.tryStrategy(
1495
+ strategies[current],
1496
+ minPriority,
1497
+ { timeout: timeout, failFast: this.failFast },
1498
+ tryNextStrategy
1499
+ );
1500
+
1501
+ return {
1502
+ abort: function() {
1503
+ runner.abort();
1504
+ },
1505
+ forceMinPriority: function(p) {
1506
+ minPriority = p;
1507
+ if (runner) {
1508
+ runner.forceMinPriority(p);
1509
+ }
1510
+ }
1511
+ };
1512
+ };
1513
+
1514
+ /** @private */
1515
+ prototype.tryStrategy = function(strategy, minPriority, options, callback) {
1516
+ var timer = null;
1517
+ var runner = null;
1518
+
1519
+ runner = strategy.connect(minPriority, function(error, connection) {
1520
+ if (error && timer && timer.isRunning() && !options.failFast) {
1521
+ // advance to the next strategy after the timeout
1522
+ return;
1523
+ }
1524
+ if (timer) {
1525
+ timer.ensureAborted();
1526
+ }
1527
+ callback(error, connection);
1528
+ });
1529
+
1530
+ if (options.timeout > 0) {
1531
+ timer = new Pusher.Timer(options.timeout, function() {
1532
+ runner.abort();
1533
+ callback(true);
1534
+ });
1535
+ }
1536
+
1537
+ return {
1538
+ abort: function() {
1539
+ if (timer) {
1540
+ timer.ensureAborted();
1541
+ }
1542
+ runner.abort();
1543
+ },
1544
+ forceMinPriority: function(p) {
1545
+ runner.forceMinPriority(p);
1546
+ }
1547
+ };
1548
+ };
1549
+
1550
+ Pusher.SequentialStrategy = SequentialStrategy;
1551
+ }).call(this);
1552
+
1553
+ ;(function() {
1554
+ /** Provides a strategy interface for transports.
1555
+ *
1556
+ * @param {String} name
1557
+ * @param {Number} priority
1558
+ * @param {Class} transport
1559
+ * @param {Object} options
1560
+ */
1561
+ function TransportStrategy(name, priority, transport, options) {
1562
+ this.name = name;
1563
+ this.priority = priority;
1564
+ this.transport = transport;
1565
+ this.options = options || {};
1566
+ }
1567
+ var prototype = TransportStrategy.prototype;
1568
+
1569
+ /** Returns whether the transport is supported in the browser.
1570
+ *
1571
+ * @returns {Boolean}
1572
+ */
1573
+ prototype.isSupported = function() {
1574
+ return this.transport.isSupported({
1575
+ disableFlash: !!this.options.disableFlash
1576
+ });
1577
+ };
1578
+
1579
+ /** Launches a connection attempt and returns a strategy runner.
1580
+ *
1581
+ * @param {Function} callback
1582
+ * @return {Object} strategy runner
1583
+ */
1584
+ prototype.connect = function(minPriority, callback) {
1585
+ if (!this.transport.isSupported()) {
1586
+ return failAttempt(new Pusher.Errors.UnsupportedStrategy(), callback);
1587
+ } else if (this.priority < minPriority) {
1588
+ return failAttempt(new Pusher.Errors.TransportPriorityTooLow(), callback);
1589
+ }
1590
+
1591
+ var self = this;
1592
+ var connection = this.transport.createConnection(
1593
+ this.name, this.priority, this.options.key, this.options
1594
+ );
1595
+
1596
+ var onInitialized = function() {
1597
+ connection.unbind("initialized", onInitialized);
1598
+ connection.connect();
1599
+ };
1600
+ var onOpen = function() {
1601
+ unbindListeners();
1602
+ callback(null, connection);
1603
+ };
1604
+ var onError = function(error) {
1605
+ unbindListeners();
1606
+ callback(error);
1607
+ };
1608
+ var onClosed = function() {
1609
+ unbindListeners();
1610
+ callback(new Pusher.Errors.TransportClosed(this.transport));
1611
+ };
1612
+
1613
+ var unbindListeners = function() {
1614
+ connection.unbind("initialized", onInitialized);
1615
+ connection.unbind("open", onOpen);
1616
+ connection.unbind("error", onError);
1617
+ connection.unbind("closed", onClosed);
1618
+ };
1619
+
1620
+ connection.bind("initialized", onInitialized);
1621
+ connection.bind("open", onOpen);
1622
+ connection.bind("error", onError);
1623
+ connection.bind("closed", onClosed);
1624
+
1625
+ // connect will be called automatically after initialization
1626
+ connection.initialize();
1627
+
1628
+ return {
1629
+ abort: function() {
1630
+ if (connection.state === "open") {
1631
+ return;
1632
+ }
1633
+ unbindListeners();
1634
+ connection.close();
1635
+ },
1636
+ forceMinPriority: function(p) {
1637
+ if (connection.state === "open") {
1638
+ return;
1639
+ }
1640
+ if (self.priority < p) {
1641
+ // TODO close connection in a nicer way
1642
+ connection.close();
88
1643
  }
89
1644
  }
90
- },
91
-
92
- subscribe: function(channel_name) {
93
- var self = this;
94
- var channel = this.channels.add(channel_name, this);
1645
+ };
1646
+ };
95
1647
 
96
- if (this.connection.state === 'connected') {
97
- channel.authorize(this.connection.socket_id, this.options, function(err, data) {
98
- if (err) {
99
- channel.emit('pusher:subscription_error', data);
100
- } else {
101
- self.send_event('pusher:subscribe', {
102
- channel: channel_name,
103
- auth: data.auth,
104
- channel_data: data.channel_data
105
- });
106
- }
107
- });
108
- }
109
- return channel;
110
- },
1648
+ function failAttempt(error, callback) {
1649
+ new Pusher.Timer(0, function() {
1650
+ callback(error);
1651
+ });
1652
+ return {
1653
+ abort: function() {},
1654
+ forceMinPriority: function() {}
1655
+ };
1656
+ }
111
1657
 
112
- unsubscribe: function(channel_name) {
113
- this.channels.remove(channel_name);
114
- if (this.connection.state === 'connected') {
115
- this.send_event('pusher:unsubscribe', {
116
- channel: channel_name
117
- });
118
- }
119
- },
1658
+ Pusher.TransportStrategy = TransportStrategy;
1659
+ }).call(this);
120
1660
 
121
- send_event: function(event_name, data, channel) {
122
- return this.connection.send_event(event_name, data, channel);
123
- },
1661
+ ;(function() {
1662
+ /** Handles common logic for all transports.
1663
+ *
1664
+ * Transport is a low-level connection object that wraps a connection method
1665
+ * and exposes a simple evented interface for the connection state and
1666
+ * messaging. It does not implement Pusher-specific WebSocket protocol.
1667
+ *
1668
+ * Additionally, it fetches resources needed for transport to work and exposes
1669
+ * an interface for querying transport support and its features.
1670
+ *
1671
+ * This is an abstract class, please do not instantiate it.
1672
+ *
1673
+ * States:
1674
+ * - new - initial state after constructing the object
1675
+ * - initializing - during initialization phase, usually fetching resources
1676
+ * - intialized - ready to establish a connection
1677
+ * - connection - when connection is being established
1678
+ * - open - when connection ready to be used
1679
+ * - closed - after connection was closed be either side
1680
+ *
1681
+ * Emits:
1682
+ * - error - after the connection raised an error
1683
+ *
1684
+ * Options:
1685
+ * - encrypted - whether connection should use ssl
1686
+ * - hostEncrypted - host to connect to when connection is encrypted
1687
+ * - hostUnencrypted - host to connect to when connection is not encrypted
1688
+ *
1689
+ * @param {String} key application key
1690
+ * @param {Object} options
1691
+ */
1692
+ function AbstractTransport(name, priority, key, options) {
1693
+ Pusher.EventsDispatcher.call(this);
124
1694
 
125
- checkAppKey: function() {
126
- if (!this.key) {
127
- // do not allow undefined, null or empty string
128
- Pusher.warn('Warning', 'You must pass your app key when you instantiate Pusher.');
129
- }
130
- }
1695
+ this.name = name;
1696
+ this.priority = priority;
1697
+ this.key = key;
1698
+ this.state = "new";
1699
+ this.timeline = options.timeline;
1700
+ this.id = this.timeline.generateUniqueID();
1701
+
1702
+ this.options = {
1703
+ encrypted: Boolean(options.encrypted),
1704
+ hostUnencrypted: options.hostUnencrypted,
1705
+ hostEncrypted: options.hostEncrypted
1706
+ };
1707
+ }
1708
+ var prototype = AbstractTransport.prototype;
1709
+ Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype);
1710
+
1711
+ /** Checks whether the transport is supported in the browser.
1712
+ *
1713
+ * @returns {Boolean}
1714
+ */
1715
+ AbstractTransport.isSupported = function() {
1716
+ return false;
131
1717
  };
132
1718
 
133
- Pusher.Util = {
134
- extend: function extend(target, extensions) {
135
- for (var property in extensions) {
136
- if (extensions[property] && extensions[property].constructor &&
137
- extensions[property].constructor === Object) {
138
- target[property] = extend(target[property] || {}, extensions[property]);
139
- } else {
140
- target[property] = extensions[property];
141
- }
142
- }
143
- return target;
144
- },
1719
+ /** Checks whether the transport handles ping/pong on itself.
1720
+ *
1721
+ * @return {Boolean}
1722
+ */
1723
+ prototype.supportsPing = function() {
1724
+ return false;
1725
+ };
145
1726
 
146
- stringify: function stringify() {
147
- var m = ["Pusher"]
148
- for (var i = 0; i < arguments.length; i++){
149
- if (typeof arguments[i] === "string") {
150
- m.push(arguments[i])
151
- } else {
152
- if (window['JSON'] == undefined) {
153
- m.push(arguments[i].toString());
154
- } else {
155
- m.push(JSON.stringify(arguments[i]))
156
- }
157
- }
158
- };
159
- return m.join(" : ")
160
- },
1727
+ /** Initializes the transport.
1728
+ *
1729
+ * Fetches resources if needed and then transitions to initialized.
1730
+ */
1731
+ prototype.initialize = function() {
1732
+ this.timeline.info(this.buildTimelineMessage({
1733
+ transport: this.name + (this.options.encrypted ? "s" : "")
1734
+ }));
1735
+ this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
1736
+
1737
+ this.changeState("initialized");
1738
+ };
161
1739
 
162
- arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf
163
- var nativeIndexOf = Array.prototype.indexOf;
164
- if (array == null) return -1;
165
- if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
166
- for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
167
- return -1;
1740
+ /** Tries to establish a connection.
1741
+ *
1742
+ * @returns {Boolean} false if transport is in invalid state
1743
+ */
1744
+ prototype.connect = function() {
1745
+ var url = this.getURL(this.key, this.options);
1746
+ this.timeline.debug(this.buildTimelineMessage({
1747
+ method: "connect",
1748
+ url: url
1749
+ }));
1750
+
1751
+ if (this.socket || this.state !== "initialized") {
1752
+ return false;
168
1753
  }
1754
+
1755
+ this.socket = this.createSocket(url);
1756
+ this.bindListeners();
1757
+
1758
+ Pusher.debug("Connecting", { transport: this.name, url: url });
1759
+ this.changeState("connecting");
1760
+ return true;
169
1761
  };
170
1762
 
171
- // To receive log output provide a Pusher.log function, for example
172
- // Pusher.log = function(m){console.log(m)}
173
- Pusher.debug = function() {
174
- if (!Pusher.log) return
175
- Pusher.log(Pusher.Util.stringify.apply(this, arguments))
176
- }
177
- Pusher.warn = function() {
178
- if (window.console && window.console.warn) {
179
- window.console.warn(Pusher.Util.stringify.apply(this, arguments));
1763
+ /** Closes the connection.
1764
+ *
1765
+ * @return {Boolean} true if there was a connection to close
1766
+ */
1767
+ prototype.close = function() {
1768
+ this.timeline.debug(this.buildTimelineMessage({ method: "close" }));
1769
+
1770
+ if (this.socket) {
1771
+ this.socket.close();
1772
+ return true;
180
1773
  } else {
181
- if (!Pusher.log) return
182
- Pusher.log(Pusher.Util.stringify.apply(this, arguments));
1774
+ return false;
183
1775
  }
184
1776
  };
185
1777
 
186
- // Pusher defaults
187
- Pusher.VERSION = '1.12.7';
188
- // WS connection parameters
189
- Pusher.host = 'ws.pusherapp.com';
190
- Pusher.ws_port = 80;
191
- Pusher.wss_port = 443;
192
- // SockJS fallback parameters
193
- Pusher.sockjs_host = 'sockjs.pusher.com';
194
- Pusher.sockjs_http_port = 80
195
- Pusher.sockjs_https_port = 443
196
- Pusher.sockjs_path = "/pusher"
197
- // Other settings
198
- Pusher.channel_auth_endpoint = '/pusher/auth';
199
- Pusher.cdn_http = 'http://js.pusher.com/'
200
- Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'
201
- Pusher.dependency_suffix = '';
202
- Pusher.channel_auth_transport = 'ajax';
203
- Pusher.activity_timeout = 120000;
204
- Pusher.pong_timeout = 30000;
1778
+ /** Sends data over the open connection.
1779
+ *
1780
+ * @param {String} data
1781
+ * @return {Boolean} true only when in the "open" state
1782
+ */
1783
+ prototype.send = function(data) {
1784
+ this.timeline.debug(this.buildTimelineMessage({
1785
+ method: "send",
1786
+ data: data
1787
+ }));
205
1788
 
206
- Pusher.isReady = false;
207
- Pusher.ready = function() {
208
- Pusher.isReady = true;
209
- for (var i = 0, l = Pusher.instances.length; i < l; i++) {
210
- Pusher.instances[i].connect();
1789
+ if (this.state === "open") {
1790
+ // Workaround for MobileSafari bug (see https://gist.github.com/2052006)
1791
+ var self = this;
1792
+ setTimeout(function() {
1793
+ self.socket.send(data);
1794
+ }, 0);
1795
+ return true;
1796
+ } else {
1797
+ return false;
211
1798
  }
212
1799
  };
213
1800
 
214
- this.Pusher = Pusher;
215
- }).call(this);
216
-
217
- ;(function() {
218
- /* Abstract event binding
219
- Example:
1801
+ prototype.requestPing = function() {
1802
+ this.emit("ping_request");
1803
+ };
220
1804
 
221
- var MyEventEmitter = function(){};
222
- MyEventEmitter.prototype = new Pusher.EventsDispatcher;
1805
+ /** @protected */
1806
+ prototype.onOpen = function() {
1807
+ this.changeState("open");
1808
+ this.socket.onopen = undefined;
1809
+ };
223
1810
 
224
- var emitter = new MyEventEmitter();
1811
+ /** @protected */
1812
+ prototype.onError = function(error) {
1813
+ this.emit("error", { type: 'WebSocketError', error: error });
1814
+ this.timeline.error(this.buildTimelineMessage({
1815
+ error: getErrorDetails(error)
1816
+ }));
1817
+ };
225
1818
 
226
- // Bind to single event
227
- emitter.bind('foo_event', function(data){ alert(data)} );
1819
+ /** @protected */
1820
+ prototype.onClose = function(closeEvent) {
1821
+ this.changeState("closed", closeEvent);
1822
+ this.socket = undefined;
1823
+ };
228
1824
 
229
- // Bind to all
230
- emitter.bind_all(function(eventName, data){ alert(data) });
1825
+ /** @protected */
1826
+ prototype.onMessage = function(message) {
1827
+ this.timeline.debug(this.buildTimelineMessage({ message: message.data }));
1828
+ this.emit("message", message);
1829
+ };
231
1830
 
232
- --------------------------------------------------------*/
1831
+ /** @protected */
1832
+ prototype.bindListeners = function() {
1833
+ var self = this;
233
1834
 
234
- function CallbackRegistry() {
235
- this._callbacks = {};
1835
+ this.socket.onopen = function() { self.onOpen(); };
1836
+ this.socket.onerror = function(error) { self.onError(error); };
1837
+ this.socket.onclose = function(closeEvent) { self.onClose(closeEvent); };
1838
+ this.socket.onmessage = function(message) { self.onMessage(message); };
236
1839
  };
237
1840
 
238
- CallbackRegistry.prototype.get = function(eventName) {
239
- return this._callbacks[this._prefix(eventName)];
1841
+ /** @protected */
1842
+ prototype.createSocket = function(url) {
1843
+ return null;
240
1844
  };
241
1845
 
242
- CallbackRegistry.prototype.add = function(eventName, callback) {
243
- var prefixedEventName = this._prefix(eventName);
244
- this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || [];
245
- this._callbacks[prefixedEventName].push(callback);
1846
+ /** @protected */
1847
+ prototype.getScheme = function() {
1848
+ return this.options.encrypted ? "wss" : "ws";
246
1849
  };
247
1850
 
248
- CallbackRegistry.prototype.remove = function(eventName, callback) {
249
- if(this.get(eventName)) {
250
- var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback);
251
- this._callbacks[this._prefix(eventName)].splice(index, 1);
1851
+ /** @protected */
1852
+ prototype.getBaseURL = function() {
1853
+ var host;
1854
+ if (this.options.encrypted) {
1855
+ host = this.options.hostEncrypted;
1856
+ } else {
1857
+ host = this.options.hostUnencrypted;
252
1858
  }
1859
+ return this.getScheme() + "://" + host;
253
1860
  };
254
1861
 
255
- CallbackRegistry.prototype._prefix = function(eventName) {
256
- return "_" + eventName;
1862
+ /** @protected */
1863
+ prototype.getPath = function() {
1864
+ return "/app/" + this.key;
257
1865
  };
258
1866
 
1867
+ /** @protected */
1868
+ prototype.getQueryString = function() {
1869
+ return "?protocol=" + Pusher.PROTOCOL +
1870
+ "&client=js&version=" + Pusher.VERSION;
1871
+ };
259
1872
 
260
- function EventsDispatcher(failThrough) {
261
- this.callbacks = new CallbackRegistry();
262
- this.global_callbacks = [];
263
- // Run this function when dispatching an event when no callbacks defined
264
- this.failThrough = failThrough;
265
- }
1873
+ /** @protected */
1874
+ prototype.getURL = function() {
1875
+ return this.getBaseURL() + this.getPath() + this.getQueryString();
1876
+ };
266
1877
 
267
- EventsDispatcher.prototype.bind = function(eventName, callback) {
268
- this.callbacks.add(eventName, callback);
269
- return this;// chainable
1878
+ /** @protected */
1879
+ prototype.changeState = function(state, params) {
1880
+ this.state = state;
1881
+ this.timeline.info(this.buildTimelineMessage({
1882
+ state: state,
1883
+ params: params
1884
+ }));
1885
+ this.emit(state, params);
270
1886
  };
271
1887
 
272
- EventsDispatcher.prototype.unbind = function(eventName, callback) {
273
- this.callbacks.remove(eventName, callback);
274
- return this;
1888
+ /** @protected */
1889
+ prototype.buildTimelineMessage = function(message) {
1890
+ return Pusher.Util.extend({ cid: this.id }, message);
275
1891
  };
276
1892
 
277
- EventsDispatcher.prototype.emit = function(eventName, data) {
278
- // Global callbacks
279
- for (var i = 0; i < this.global_callbacks.length; i++) {
280
- this.global_callbacks[i](eventName, data);
1893
+ function getErrorDetails(error) {
1894
+ if (typeof error === "string") {
1895
+ return error;
281
1896
  }
282
-
283
- // Event callbacks
284
- var callbacks = this.callbacks.get(eventName);
285
- if (callbacks) {
286
- for (var i = 0; i < callbacks.length; i++) {
287
- callbacks[i](data);
288
- }
289
- } else if (this.failThrough) {
290
- this.failThrough(eventName, data)
1897
+ if (typeof error === "object") {
1898
+ return Pusher.Util.mapObject(error, function(value) {
1899
+ var valueType = typeof value;
1900
+ if (valueType === "object" || valueType == "function") {
1901
+ return valueType;
1902
+ }
1903
+ return value;
1904
+ });
291
1905
  }
1906
+ return typeof error;
1907
+ }
292
1908
 
293
- return this;
294
- };
295
-
296
- EventsDispatcher.prototype.bind_all = function(callback) {
297
- this.global_callbacks.push(callback);
298
- return this;
299
- };
300
-
301
- this.Pusher.EventsDispatcher = EventsDispatcher;
1909
+ Pusher.AbstractTransport = AbstractTransport;
302
1910
  }).call(this);
303
1911
 
304
1912
  ;(function() {
305
- var Pusher = this.Pusher;
306
-
307
- /*-----------------------------------------------
308
- Helpers:
309
- -----------------------------------------------*/
310
-
311
- function capitalize(str) {
312
- return str.substr(0, 1).toUpperCase() + str.substr(1);
1913
+ /** Transport using Flash to emulate WebSockets.
1914
+ *
1915
+ * @see AbstractTransport
1916
+ */
1917
+ function FlashTransport(name, priority, key, options) {
1918
+ Pusher.AbstractTransport.call(this, name, priority, key, options);
313
1919
  }
1920
+ var prototype = FlashTransport.prototype;
1921
+ Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
1922
+
1923
+ /** Creates a new instance of FlashTransport.
1924
+ *
1925
+ * @param {String} key
1926
+ * @param {Object} options
1927
+ * @return {FlashTransport}
1928
+ */
1929
+ FlashTransport.createConnection = function(name, priority, key, options) {
1930
+ return new FlashTransport(name, priority, key, options);
1931
+ };
314
1932
 
315
-
316
- function safeCall(method, obj, data) {
317
- if (obj[method] !== undefined) {
318
- obj[method](data);
1933
+ /** Checks whether Flash is supported in the browser.
1934
+ *
1935
+ * It is possible to disable flash by passing an envrionment object with the
1936
+ * disableFlash property set to true.
1937
+ *
1938
+ * @see AbstractTransport.isSupported
1939
+ * @param {Object} environment
1940
+ * @returns {Boolean}
1941
+ */
1942
+ FlashTransport.isSupported = function(environment) {
1943
+ if (environment && environment.disableFlash) {
1944
+ return false;
319
1945
  }
320
- }
1946
+ try {
1947
+ return !!(new ActiveXObject('ShockwaveFlash.ShockwaveFlash'));
1948
+ } catch (e) {
1949
+ return navigator.mimeTypes["application/x-shockwave-flash"] !== undefined;
1950
+ }
1951
+ };
321
1952
 
322
- /*-----------------------------------------------
323
- The State Machine
324
- -----------------------------------------------*/
325
- function Machine(initialState, transitions, stateActions) {
326
- Pusher.EventsDispatcher.call(this);
1953
+ /** Fetches flashfallback dependency if needed.
1954
+ *
1955
+ * Sets WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR to true (if not set before)
1956
+ * and WEB_SOCKET_SWF_LOCATION to Pusher's cdn before loading Flash resources.
1957
+ *
1958
+ * @see AbstractTransport.prototype.initialize
1959
+ */
1960
+ prototype.initialize = function() {
1961
+ var self = this;
327
1962
 
328
- this.state = undefined;
329
- this.errors = [];
1963
+ this.timeline.info(this.buildTimelineMessage({
1964
+ transport: this.name + (this.options.encrypted ? "s" : "")
1965
+ }));
1966
+ this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
1967
+ this.changeState("initializing");
330
1968
 
331
- // functions for each state
332
- this.stateActions = stateActions;
1969
+ if (window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR === undefined) {
1970
+ window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
1971
+ }
1972
+ window.WEB_SOCKET_SWF_LOCATION = Pusher.Dependencies.getRoot() +
1973
+ "/WebSocketMain.swf";
1974
+ Pusher.Dependencies.load("flashfallback", function() {
1975
+ self.changeState("initialized");
1976
+ });
1977
+ };
333
1978
 
334
- // set up the transitions
335
- this.transitions = transitions;
1979
+ /** @protected */
1980
+ prototype.createSocket = function(url) {
1981
+ return new FlashWebSocket(url);
1982
+ };
336
1983
 
337
- this.transition(initialState);
1984
+ /** @protected */
1985
+ prototype.getQueryString = function() {
1986
+ return Pusher.AbstractTransport.prototype.getQueryString.call(this) +
1987
+ "&flash=true";
338
1988
  };
339
1989
 
340
- Machine.prototype.transition = function(nextState, data) {
341
- var prevState = this.state;
342
- var stateCallbacks = this.stateActions;
1990
+ Pusher.FlashTransport = FlashTransport;
1991
+ }).call(this);
343
1992
 
344
- if (prevState && (Pusher.Util.arrayIndexOf(this.transitions[prevState], nextState) == -1)) {
345
- this.emit('invalid_transition_attempt', {
346
- oldState: prevState,
347
- newState: nextState
348
- });
1993
+ ;(function() {
1994
+ /** Fallback transport using SockJS.
1995
+ *
1996
+ * @see AbstractTransport
1997
+ */
1998
+ function SockJSTransport(name, priority, key, options) {
1999
+ Pusher.AbstractTransport.call(this, name, priority, key, options);
2000
+ }
2001
+ var prototype = SockJSTransport.prototype;
2002
+ Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
2003
+
2004
+ /** Creates a new instance of SockJSTransport.
2005
+ *
2006
+ * @param {String} key
2007
+ * @param {Object} options
2008
+ * @return {SockJSTransport}
2009
+ */
2010
+ SockJSTransport.createConnection = function(name, priority, key, options) {
2011
+ return new SockJSTransport(name, priority, key, options);
2012
+ };
349
2013
 
350
- throw new Error('Invalid transition [' + prevState + ' to ' + nextState + ']');
351
- }
2014
+ /** Assumes that SockJS is always supported.
2015
+ *
2016
+ * @returns {Boolean} always true
2017
+ */
2018
+ SockJSTransport.isSupported = function() {
2019
+ return true;
2020
+ };
352
2021
 
353
- // exit
354
- safeCall(prevState + 'Exit', stateCallbacks, data);
2022
+ /** Fetches sockjs dependency if needed.
2023
+ *
2024
+ * @see AbstractTransport.prototype.initialize
2025
+ */
2026
+ prototype.initialize = function() {
2027
+ var self = this;
355
2028
 
356
- // tween
357
- safeCall(prevState + 'To' + capitalize(nextState), stateCallbacks, data);
2029
+ this.timeline.info(this.buildTimelineMessage({
2030
+ transport: this.name + (this.options.encrypted ? "s" : "")
2031
+ }));
2032
+ this.timeline.debug(this.buildTimelineMessage({ method: "initialize" }));
358
2033
 
359
- // pre
360
- safeCall(nextState + 'Pre', stateCallbacks, data);
2034
+ this.changeState("initializing");
2035
+ Pusher.Dependencies.load("sockjs", function() {
2036
+ self.changeState("initialized");
2037
+ });
2038
+ };
361
2039
 
362
- // change state:
363
- this.state = nextState;
2040
+ /** Always returns true, since SockJS handles ping on its own.
2041
+ *
2042
+ * @returns {Boolean} always true
2043
+ */
2044
+ prototype.supportsPing = function() {
2045
+ return true;
2046
+ };
364
2047
 
365
- // handy to bind to
366
- this.emit('state_change', {
367
- oldState: prevState,
368
- newState: nextState
2048
+ /** @protected */
2049
+ prototype.createSocket = function(url) {
2050
+ // exclude iframe transports until we link to correct SockJS version
2051
+ // inside the iframe
2052
+ return new SockJS(url, null, {
2053
+ js_path: Pusher.Dependencies.getPath("sockjs", {
2054
+ encrypted: this.options.encrypted
2055
+ })
369
2056
  });
2057
+ };
370
2058
 
371
- // Post:
372
- safeCall(nextState + 'Post', stateCallbacks, data);
2059
+ /** @protected */
2060
+ prototype.getScheme = function() {
2061
+ return this.options.encrypted ? "https" : "http";
373
2062
  };
374
2063
 
375
- Machine.prototype.is = function(state) {
376
- return this.state === state;
2064
+ /** @protected */
2065
+ prototype.getPath = function() {
2066
+ return "/pusher";
377
2067
  };
378
2068
 
379
- Machine.prototype.isNot = function(state) {
380
- return this.state !== state;
2069
+ /** @protected */
2070
+ prototype.getQueryString = function() {
2071
+ return "";
381
2072
  };
382
2073
 
383
- Pusher.Util.extend(Machine.prototype, Pusher.EventsDispatcher.prototype);
2074
+ /** Handles opening a SockJS connection to Pusher.
2075
+ *
2076
+ * Since SockJS does not handle custom paths, we send it immediately after
2077
+ * establishing the connection.
2078
+ *
2079
+ * @protected
2080
+ */
2081
+ prototype.onOpen = function() {
2082
+ this.socket.send(JSON.stringify({
2083
+ path: Pusher.AbstractTransport.prototype.getPath.call(this) +
2084
+ Pusher.AbstractTransport.prototype.getQueryString.call(this)
2085
+ }));
2086
+ this.changeState("open");
2087
+ this.socket.onopen = undefined;
2088
+ };
384
2089
 
385
- this.Pusher.Machine = Machine;
2090
+ Pusher.SockJSTransport = SockJSTransport;
386
2091
  }).call(this);
387
2092
 
388
2093
  ;(function() {
389
- /*
390
- A little bauble to interface with window.navigator.onLine,
391
- window.ononline and window.onoffline. Easier to mock.
392
- */
2094
+ /** WebSocket transport.
2095
+ *
2096
+ * @see AbstractTransport
2097
+ */
2098
+ function WSTransport(name, priority, key, options) {
2099
+ Pusher.AbstractTransport.call(this, name, priority, key, options);
2100
+ }
2101
+ var prototype = WSTransport.prototype;
2102
+ Pusher.Util.extend(prototype, Pusher.AbstractTransport.prototype);
2103
+
2104
+ /** Creates a new instance of WSTransport.
2105
+ *
2106
+ * @param {String} key
2107
+ * @param {Object} options
2108
+ * @return {WSTransport}
2109
+ */
2110
+ WSTransport.createConnection = function(name, priority, key, options) {
2111
+ return new WSTransport(name, priority, key, options);
2112
+ };
393
2113
 
394
- var NetInfo = function() {
395
- var self = this;
396
- Pusher.EventsDispatcher.call(this);
397
- // This is okay, as IE doesn't support this stuff anyway.
398
- if (window.addEventListener !== undefined) {
399
- window.addEventListener("online", function() {
400
- self.emit('online', null);
401
- }, false);
402
- window.addEventListener("offline", function() {
403
- self.emit('offline', null);
404
- }, false);
405
- }
2114
+ /** Checks whether the browser supports WebSockets in any form.
2115
+ *
2116
+ * @returns {Boolean} true if browser supports WebSockets
2117
+ */
2118
+ WSTransport.isSupported = function() {
2119
+ return window.WebSocket !== undefined || window.MozWebSocket !== undefined;
406
2120
  };
407
2121
 
408
- // Offline means definitely offline (no connection to router).
409
- // Inverse does NOT mean definitely online (only currently supported in Safari
410
- // and even there only means the device has a connection to the router).
411
- NetInfo.prototype.isOnLine = function() {
412
- if (window.navigator.onLine === undefined) {
413
- return true;
414
- } else {
415
- return window.navigator.onLine;
416
- }
2122
+ /** @protected */
2123
+ prototype.createSocket = function(url) {
2124
+ var constructor = window.WebSocket || window.MozWebSocket;
2125
+ return new constructor(url);
417
2126
  };
418
2127
 
419
- Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
2128
+ /** @protected */
2129
+ prototype.getQueryString = function() {
2130
+ return Pusher.AbstractTransport.prototype.getQueryString.call(this) +
2131
+ "&flash=false";
2132
+ };
420
2133
 
421
- this.Pusher.NetInfo = NetInfo;
2134
+ Pusher.WSTransport = WSTransport;
422
2135
  }).call(this);
423
2136
 
424
2137
  ;(function() {
425
- var Pusher = this.Pusher;
2138
+ function AssistantToTheTransportManager(manager, transport, options) {
2139
+ this.manager = manager;
2140
+ this.transport = transport;
2141
+ this.minPingDelay = options.minPingDelay || 10000;
2142
+ this.maxPingDelay = options.maxPingDelay || Pusher.activity_timeout;
2143
+ this.pingDelay = null;
2144
+ }
2145
+ var prototype = AssistantToTheTransportManager.prototype;
426
2146
 
427
- var machineTransitions = {
428
- 'initialized': ['waiting', 'failed'],
429
- 'waiting': ['connecting', 'permanentlyClosed'],
430
- 'connecting': ['open', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
431
- 'open': ['connected', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
432
- 'connected': ['permanentlyClosing', 'waiting'],
433
- 'impermanentlyClosing': ['waiting', 'permanentlyClosing'],
434
- 'permanentlyClosing': ['permanentlyClosed'],
435
- 'permanentlyClosed': ['waiting', 'failed'],
436
- 'failed': ['permanentlyClosed']
437
- };
2147
+ prototype.createConnection = function(name, priority, key, options) {
2148
+ var connection = this.transport.createConnection(
2149
+ name, priority, key, options
2150
+ );
438
2151
 
2152
+ var self = this;
2153
+ var openTimestamp = null;
2154
+ var pingTimer = null;
439
2155
 
440
- var OPEN_TIMEOUT_INCREMENT = 2000;
441
- var CONNECTED_TIMEOUT_INCREMENT = 2000;
2156
+ var onOpen = function() {
2157
+ connection.unbind("open", onOpen);
442
2158
 
443
- var MAX_OPEN_TIMEOUT = 10000;
444
- var MAX_CONNECTED_TIMEOUT = 10000;
2159
+ openTimestamp = Pusher.Util.now();
2160
+ if (self.pingDelay) {
2161
+ pingTimer = setInterval(function() {
2162
+ if (pingTimer) {
2163
+ connection.requestPing();
2164
+ }
2165
+ }, self.pingDelay);
2166
+ }
445
2167
 
446
- function resetConnectionParameters(connection) {
447
- connection.connectionWait = 0;
2168
+ connection.bind("closed", onClosed);
2169
+ };
2170
+ var onClosed = function(closeEvent) {
2171
+ connection.unbind("closed", onClosed);
2172
+ if (pingTimer) {
2173
+ clearInterval(pingTimer);
2174
+ pingTimer = null;
2175
+ }
448
2176
 
449
- if (Pusher.TransportType === 'native') {
450
- connection.openTimeout = 4000;
451
- } else if (Pusher.TransportType === 'flash') {
452
- connection.openTimeout = 7000;
453
- } else { // SockJS
454
- connection.openTimeout = 6000;
455
- }
456
- connection.connectedTimeout = 2000;
457
- connection.connectionSecure = connection.compulsorySecure;
458
- connection.failedAttempts = 0;
459
- }
2177
+ if (closeEvent.wasClean) {
2178
+ return;
2179
+ }
460
2180
 
461
- function Connection(key, options) {
462
- var self = this;
2181
+ if (openTimestamp) {
2182
+ var lifespan = Pusher.Util.now() - openTimestamp;
2183
+ if (lifespan < 2 * self.maxPingDelay) {
2184
+ self.manager.reportDeath();
2185
+ self.pingDelay = Math.max(lifespan / 2, self.minPingDelay);
2186
+ }
2187
+ }
2188
+ };
463
2189
 
464
- Pusher.EventsDispatcher.call(this);
2190
+ connection.bind("open", onOpen);
2191
+ return connection;
2192
+ };
465
2193
 
466
- this.ping = true
467
- this.options = Pusher.Util.extend({encrypted: false}, options);
2194
+ prototype.isSupported = function() {
2195
+ return this.manager.isAlive() && this.transport.isSupported();
2196
+ };
468
2197
 
469
- this.netInfo = new Pusher.NetInfo();
2198
+ Pusher.AssistantToTheTransportManager = AssistantToTheTransportManager;
2199
+ }).call(this);
470
2200
 
471
- this.netInfo.bind('online', function(){
472
- if (self._machine.is('waiting')) {
473
- self._machine.transition('connecting');
474
- updateState('connecting');
475
- }
2201
+ ;(function() {
2202
+ function TransportManager(options) {
2203
+ this.options = options || {};
2204
+ this.livesLeft = this.options.lives || Infinity;
2205
+ }
2206
+ var prototype = TransportManager.prototype;
2207
+
2208
+ prototype.getAssistant = function(transport) {
2209
+ return new Pusher.AssistantToTheTransportManager(this, transport, {
2210
+ minPingDelay: this.options.minPingDelay,
2211
+ maxPingDelay: this.options.maxPingDelay
476
2212
  });
2213
+ };
477
2214
 
478
- this.netInfo.bind('offline', function() {
479
- if (self._machine.is('connected')) {
480
- // These are for Chrome 15, which ends up
481
- // having two sockets hanging around.
482
- self.socket.onclose = undefined;
483
- self.socket.onmessage = undefined;
484
- self.socket.onerror = undefined;
485
- self.socket.onopen = undefined;
2215
+ prototype.isAlive = function() {
2216
+ return this.livesLeft > 0;
2217
+ };
486
2218
 
487
- self.socket.close();
488
- self.socket = undefined;
489
- self._machine.transition('waiting');
490
- }
491
- });
2219
+ prototype.reportDeath = function() {
2220
+ this.livesLeft -= 1;
2221
+ };
2222
+
2223
+ Pusher.TransportManager = TransportManager;
2224
+ }).call(this);
492
2225
 
493
- // define the state machine that runs the connection
494
- this._machine = new Pusher.Machine('initialized', machineTransitions, {
495
- initializedPre: function() {
496
- self.compulsorySecure = self.options.encrypted;
2226
+ ;(function() {
2227
+ var StrategyBuilder = {
2228
+ /** Transforms a JSON scheme to a strategy tree.
2229
+ *
2230
+ * @param {Array} scheme JSON strategy scheme
2231
+ * @param {Object} options a hash of symbols to be included in the scheme
2232
+ * @returns {Strategy} strategy tree that's represented by the scheme
2233
+ */
2234
+ build: function(scheme, options) {
2235
+ var context = Pusher.Util.extend({}, globalContext, options);
2236
+ return evaluate(scheme, context)[1].strategy;
2237
+ }
2238
+ };
497
2239
 
498
- self.key = key;
499
- self.socket = null;
500
- self.socket_id = null;
2240
+ var transports = {
2241
+ ws: Pusher.WSTransport,
2242
+ flash: Pusher.FlashTransport,
2243
+ sockjs: Pusher.SockJSTransport
2244
+ };
501
2245
 
502
- self.state = 'initialized';
503
- },
2246
+ // DSL bindings
504
2247
 
505
- waitingPre: function() {
506
- if (self.netInfo.isOnLine()) {
507
- if (self.failedAttempts < 2) {
508
- updateState('connecting');
509
- } else {
510
- updateState('unavailable');
511
- // Delay 10s between connection attempts on entering unavailable
512
- self.connectionWait = 10000;
513
- }
2248
+ function returnWithOriginalContext(f) {
2249
+ return function(context) {
2250
+ return [f.apply(this, arguments), context];
2251
+ };
2252
+ }
514
2253
 
515
- if (self.connectionWait > 0) {
516
- self.emit('connecting_in', connectionDelay());
517
- }
2254
+ var globalContext = {
2255
+ def: function(context, name, value) {
2256
+ if (context[name] !== undefined) {
2257
+ throw "Redefining symbol " + name;
2258
+ }
2259
+ context[name] = value;
2260
+ return [undefined, context];
2261
+ },
518
2262
 
519
- self._waitingTimer = setTimeout(function() {
520
- // Even when unavailable we try connecting (not changing state)
521
- self._machine.transition('connecting');
522
- }, connectionDelay());
523
- } else {
524
- updateState('unavailable');
525
- }
526
- },
2263
+ def_transport: function(context, name, type, priority, options, manager) {
2264
+ var transportClass = transports[type];
2265
+ if (!transportClass) {
2266
+ throw new Pusher.Errors.UnsupportedTransport(type);
2267
+ }
2268
+ var transportOptions = Pusher.Util.extend({}, {
2269
+ key: context.key,
2270
+ encrypted: context.encrypted,
2271
+ timeline: context.timeline,
2272
+ disableFlash: context.disableFlash
2273
+ }, options);
2274
+ if (manager) {
2275
+ transportClass = manager.getAssistant(transportClass);
2276
+ }
2277
+ var transport = new Pusher.TransportStrategy(
2278
+ name, priority, transportClass, transportOptions
2279
+ );
2280
+ var newContext = context.def(context, name, transport)[1];
2281
+ newContext.transports = context.transports || {};
2282
+ newContext.transports[name] = transport;
2283
+ return [undefined, newContext];
2284
+ },
527
2285
 
528
- waitingExit: function() {
529
- clearTimeout(self._waitingTimer);
530
- },
2286
+ transport_manager: returnWithOriginalContext(function(_, options) {
2287
+ return new Pusher.TransportManager(options);
2288
+ }),
531
2289
 
532
- connectingPre: function() {
533
- // Case that a user manages to get to the connecting
534
- // state even when offline.
535
- if (self.netInfo.isOnLine() === false) {
536
- self._machine.transition('waiting');
537
- updateState('unavailable');
2290
+ sequential: returnWithOriginalContext(function(_, options) {
2291
+ var strategies = Array.prototype.slice.call(arguments, 2);
2292
+ return new Pusher.SequentialStrategy(strategies, options);
2293
+ }),
538
2294
 
539
- return;
540
- }
2295
+ cached: returnWithOriginalContext(function(context, ttl, strategy){
2296
+ return new Pusher.CachedStrategy(strategy, context.transports, {
2297
+ ttl: ttl,
2298
+ timeline: context.timeline
2299
+ });
2300
+ }),
541
2301
 
542
- var path = connectPath(self.key);
543
- if (Pusher.TransportType === 'sockjs') {
544
- Pusher.debug('Connecting to sockjs', Pusher.sockjs);
545
- var url = buildSockJSURL(self.connectionSecure);
546
-
547
- self.ping = false
548
- self.socket = new SockJS(url);
549
- self.socket.onopen = function() {
550
- // SockJS does not yet support custom paths and query params
551
- self.socket.send(JSON.stringify({path: path}));
552
- self._machine.transition('open');
553
- }
554
- } else {
555
- var url = connectBaseURL(self.connectionSecure) + path;
556
- Pusher.debug('Connecting', url);
557
- self.socket = new Pusher.Transport(url);
558
- self.socket.onopen = function() {
559
- self._machine.transition('open');
560
- }
561
- }
2302
+ first_connected: returnWithOriginalContext(function(_, strategy) {
2303
+ return new Pusher.FirstConnectedStrategy(strategy);
2304
+ }),
2305
+
2306
+ best_connected_ever: returnWithOriginalContext(function() {
2307
+ var strategies = Array.prototype.slice.call(arguments, 1);
2308
+ return new Pusher.BestConnectedEverStrategy(strategies);
2309
+ }),
2310
+
2311
+ delayed: returnWithOriginalContext(function(_, delay, strategy) {
2312
+ return new Pusher.DelayedStrategy(strategy, { delay: delay });
2313
+ }),
2314
+
2315
+ "if": returnWithOriginalContext(function(_, test, trueBranch, falseBranch) {
2316
+ return new Pusher.IfStrategy(test, trueBranch, falseBranch);
2317
+ }),
2318
+
2319
+ is_supported: returnWithOriginalContext(function(_, strategy) {
2320
+ return function() {
2321
+ return strategy.isSupported();
2322
+ };
2323
+ })
2324
+ };
562
2325
 
563
- self.socket.onclose = transitionToWaiting;
564
- self.socket.onerror = ws_onError;
2326
+ // DSL interpreter
565
2327
 
566
- // allow time to get ws_onOpen, otherwise close socket and try again
567
- self._connectingTimer = setTimeout(TransitionToImpermanentlyClosing, self.openTimeout);
568
- },
2328
+ function isSymbol(expression) {
2329
+ return (typeof expression === "string") && expression.charAt(0) === ":";
2330
+ }
569
2331
 
570
- connectingExit: function() {
571
- clearTimeout(self._connectingTimer);
572
- self.socket.onopen = undefined; // unbind to avoid open events that are no longer relevant
573
- },
2332
+ function getSymbolValue(expression, context) {
2333
+ return context[expression.slice(1)];
2334
+ }
574
2335
 
575
- connectingToWaiting: function() {
576
- updateConnectionParameters();
2336
+ function evaluateListOfExpressions(expressions, context) {
2337
+ if (expressions.length === 0) {
2338
+ return [[], context];
2339
+ }
2340
+ var head = evaluate(expressions[0], context);
2341
+ var tail = evaluateListOfExpressions(expressions.slice(1), head[1]);
2342
+ return [[head[0]].concat(tail[0]), tail[1]];
2343
+ }
577
2344
 
578
- // FUTURE: update only ssl
579
- },
2345
+ function evaluateString(expression, context) {
2346
+ if (!isSymbol(expression)) {
2347
+ return [expression, context];
2348
+ }
2349
+ var value = getSymbolValue(expression, context);
2350
+ if (value === undefined) {
2351
+ throw "Undefined symbol " + expression;
2352
+ }
2353
+ return [value, context];
2354
+ }
580
2355
 
581
- connectingToImpermanentlyClosing: function() {
582
- updateConnectionParameters();
2356
+ function evaluateArray(expression, context) {
2357
+ if (isSymbol(expression[0])) {
2358
+ var f = getSymbolValue(expression[0], context);
2359
+ if (expression.length > 1) {
2360
+ if (typeof f !== "function") {
2361
+ throw "Calling non-function " + expression[0];
2362
+ }
2363
+ var args = [Pusher.Util.extend({}, context)].concat(
2364
+ Pusher.Util.map(expression.slice(1), function(arg) {
2365
+ return evaluate(arg, Pusher.Util.extend({}, context))[0];
2366
+ })
2367
+ );
2368
+ return f.apply(this, args);
2369
+ } else {
2370
+ return [f, context];
2371
+ }
2372
+ } else {
2373
+ return evaluateListOfExpressions(expression, context);
2374
+ }
2375
+ }
583
2376
 
584
- // FUTURE: update only timeout
585
- },
2377
+ function evaluate(expression, context) {
2378
+ var expressionType = typeof expression;
2379
+ if (typeof expression === "string") {
2380
+ return evaluateString(expression, context);
2381
+ } else if (typeof expression === "object") {
2382
+ if (expression instanceof Array && expression.length > 0) {
2383
+ return evaluateArray(expression, context);
2384
+ }
2385
+ }
2386
+ return [expression, context];
2387
+ }
586
2388
 
587
- openPre: function() {
588
- self.socket.onmessage = ws_onMessageOpen;
589
- self.socket.onerror = ws_onError;
590
- self.socket.onclose = transitionToWaiting;
2389
+ Pusher.StrategyBuilder = StrategyBuilder;
2390
+ }).call(this);
591
2391
 
592
- // allow time to get connected-to-Pusher message, otherwise close socket, try again
593
- self._openTimer = setTimeout(TransitionToImpermanentlyClosing, self.connectedTimeout);
594
- },
2392
+ ;(function() {
2393
+ /**
2394
+ * Provides Pusher protocol interface for transports.
2395
+ *
2396
+ * Emits following events:
2397
+ * - connected - after establishing connection and receiving a socket id
2398
+ * - message - on received messages
2399
+ * - ping - on ping requests
2400
+ * - pong - on pong responses
2401
+ * - error - when the transport emits an error
2402
+ * - closed - after closing the transport
2403
+ * - ssl_only - after trying to connect without ssl to a ssl-only app
2404
+ * - retry - when closed connection should be retried immediately
2405
+ * - backoff - when closed connection should be retried with a delay
2406
+ * - refused - when closed connection should not be retried
2407
+ *
2408
+ * @param {AbstractTransport} transport
2409
+ */
2410
+ function ProtocolWrapper(transport) {
2411
+ Pusher.EventsDispatcher.call(this);
2412
+ this.transport = transport;
2413
+ this.bindListeners();
2414
+ }
2415
+ var prototype = ProtocolWrapper.prototype;
595
2416
 
596
- openExit: function() {
597
- clearTimeout(self._openTimer);
598
- self.socket.onmessage = undefined; // unbind to avoid messages that are no longer relevant
599
- },
2417
+ Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype);
600
2418
 
601
- openToWaiting: function() {
602
- updateConnectionParameters();
603
- },
2419
+ /** Returns whether used transport handles ping/pong by itself
2420
+ *
2421
+ * @returns {Boolean} true if ping is handled by the transport
2422
+ */
2423
+ prototype.supportsPing = function() {
2424
+ return this.transport.supportsPing();
2425
+ };
604
2426
 
605
- openToImpermanentlyClosing: function() {
606
- updateConnectionParameters();
607
- },
2427
+ /** Sends raw data.
2428
+ *
2429
+ * @param {String} data
2430
+ */
2431
+ prototype.send = function(data) {
2432
+ return this.transport.send(data);
2433
+ };
608
2434
 
609
- connectedPre: function(socket_id) {
610
- self.socket_id = socket_id;
2435
+ /** Sends an event.
2436
+ *
2437
+ * @param {String} name
2438
+ * @param {String} data
2439
+ * @param {String} [channel]
2440
+ * @returns {Boolean} whether message was sent or not
2441
+ */
2442
+ prototype.send_event = function(name, data, channel) {
2443
+ var payload = {
2444
+ event: name,
2445
+ data: data
2446
+ };
2447
+ if (channel) {
2448
+ payload.channel = channel;
2449
+ }
611
2450
 
612
- self.socket.onmessage = ws_onMessageConnected;
613
- self.socket.onerror = ws_onError;
614
- self.socket.onclose = transitionToWaiting;
2451
+ Pusher.debug('Event sent', payload);
2452
+ return this.send(JSON.stringify(payload));
2453
+ };
615
2454
 
616
- resetConnectionParameters(self);
617
- self.connectedAt = new Date().getTime();
2455
+ /** Closes the transport. */
2456
+ prototype.close = function() {
2457
+ this.transport.close();
2458
+ };
618
2459
 
619
- resetActivityCheck();
620
- },
2460
+ /** @private */
2461
+ prototype.bindListeners = function() {
2462
+ var self = this;
621
2463
 
622
- connectedPost: function() {
623
- updateState('connected');
624
- },
2464
+ var onMessageOpen = function(message) {
2465
+ message = self.parseMessage(message);
2466
+
2467
+ if (message !== undefined) {
2468
+ if (message.event === 'pusher:connection_established') {
2469
+ self.id = message.data.socket_id;
2470
+ self.transport.unbind("message", onMessageOpen);
2471
+ self.transport.bind("message", onMessageConnected);
2472
+ self.transport.bind("ping_request", onPingRequest);
2473
+ self.emit("connected", self.id);
2474
+ } else if (message.event === "pusher:error") {
2475
+ // From protocol 6 close codes are sent only once, so this only
2476
+ // happens when connection does not support close codes
2477
+ self.handleCloseCode(message.data.code, message.data.message);
2478
+ self.transport.close();
2479
+ }
2480
+ }
2481
+ };
2482
+ var onMessageConnected = function(message) {
2483
+ message = self.parseMessage(message);
625
2484
 
626
- connectedExit: function() {
627
- stopActivityCheck();
628
- updateState('disconnected');
629
- },
2485
+ if (message !== undefined) {
2486
+ Pusher.debug('Event recd', message);
630
2487
 
631
- impermanentlyClosingPost: function() {
632
- if (self.socket) {
633
- self.socket.onclose = transitionToWaiting;
634
- self.socket.close();
2488
+ switch (message.event) {
2489
+ case 'pusher:error':
2490
+ self.emit('error', { type: 'PusherError', data: message.data });
2491
+ break;
2492
+ case 'pusher:ping':
2493
+ self.emit("ping");
2494
+ break;
2495
+ case 'pusher:pong':
2496
+ self.emit("pong");
2497
+ break;
635
2498
  }
636
- },
2499
+ self.emit('message', message);
2500
+ }
2501
+ };
2502
+ var onPingRequest = function() {
2503
+ self.emit("ping_request");
2504
+ };
2505
+ var onError = function(error) {
2506
+ self.emit("error", { type: "WebSocketError", error: error });
2507
+ };
2508
+ var onClosed = function(error) {
2509
+ if (error && error.code) {
2510
+ self.handleCloseCode(error.code, error.reason);
2511
+ }
2512
+ self.transport.unbind("message", onMessageOpen);
2513
+ self.transport.unbind("message", onMessageConnected);
2514
+ self.transport.unbind("ping_request", onPingRequest);
2515
+ self.transport.unbind("error", onError);
2516
+ self.transport.unbind("closed", onClosed);
2517
+ self.transport = null;
2518
+ self.emit("closed");
2519
+ };
637
2520
 
638
- permanentlyClosingPost: function() {
639
- if (self.socket) {
640
- self.socket.onclose = function() {
641
- resetConnectionParameters(self);
642
- self._machine.transition('permanentlyClosed');
643
- };
2521
+ this.transport.bind("message", onMessageOpen);
2522
+ this.transport.bind("error", onError);
2523
+ this.transport.bind("closed", onClosed);
2524
+ };
644
2525
 
645
- self.socket.close();
646
- } else {
647
- resetConnectionParameters(self);
648
- self._machine.transition('permanentlyClosed');
2526
+ /** @private */
2527
+ prototype.parseMessage = function(message) {
2528
+ try {
2529
+ var params = JSON.parse(message.data);
2530
+
2531
+ if (typeof params.data === 'string') {
2532
+ try {
2533
+ params.data = JSON.parse(params.data);
2534
+ } catch (e) {
2535
+ if (!(e instanceof SyntaxError)) {
2536
+ throw e;
2537
+ }
649
2538
  }
650
- },
2539
+ }
651
2540
 
652
- failedPre: function() {
653
- updateState('failed');
654
- Pusher.debug('WebSockets are not available in this browser.');
655
- },
2541
+ return params;
2542
+ } catch (e) {
2543
+ this.emit(
2544
+ 'error', { type: 'MessageParseError', error: e, data: message.data}
2545
+ );
2546
+ }
2547
+ };
656
2548
 
657
- permanentlyClosedPost: function() {
658
- updateState('disconnected');
2549
+ /** @private */
2550
+ prototype.handleCloseCode = function(code, message) {
2551
+ this.emit(
2552
+ 'error', { type: 'PusherError', data: { code: code, message: message } }
2553
+ );
2554
+
2555
+ // See:
2556
+ // 1. https://developer.mozilla.org/en-US/docs/WebSockets/WebSockets_reference/CloseEvent
2557
+ // 2. http://pusher.com/docs/pusher_protocol
2558
+ if (code < 4000) {
2559
+ // ignore 1000 CLOSE_NORMAL, 1001 CLOSE_GOING_AWAY,
2560
+ // 1005 CLOSE_NO_STATUS, 1006 CLOSE_ABNORMAL
2561
+ // ignore 1007...3999
2562
+ // handle 1002 CLOSE_PROTOCOL_ERROR, 1003 CLOSE_UNSUPPORTED,
2563
+ // 1004 CLOSE_TOO_LARGE
2564
+ if (code >= 1002 && code <= 1004) {
2565
+ this.emit("backoff");
659
2566
  }
660
- });
2567
+ } else if (code === 4000) {
2568
+ this.emit("ssl_only");
2569
+ } else if (code < 4100) {
2570
+ this.emit("refused");
2571
+ } else if (code < 4200) {
2572
+ this.emit("backoff");
2573
+ } else if (code < 4300) {
2574
+ this.emit("retry");
2575
+ } else {
2576
+ // unknown error
2577
+ this.emit("refused");
2578
+ }
2579
+ };
661
2580
 
662
- /*-----------------------------------------------
663
- -----------------------------------------------*/
2581
+ Pusher.ProtocolWrapper = ProtocolWrapper;
2582
+ }).call(this);
664
2583
 
665
- function updateConnectionParameters() {
666
- if (self.openTimeout < MAX_OPEN_TIMEOUT) {
667
- self.openTimeout += OPEN_TIMEOUT_INCREMENT;
668
- }
2584
+ ;(function() {
2585
+ /** Manages connection to Pusher.
2586
+ *
2587
+ * Uses a strategy (currently only default), timers and network availability
2588
+ * info to establish a connection and export its state. In case of failures,
2589
+ * manages reconnection attempts.
2590
+ *
2591
+ * Exports state changes as following events:
2592
+ * - "state_change", { previous: p, current: state }
2593
+ * - state
2594
+ *
2595
+ * States:
2596
+ * - initialized - initial state, never transitioned to
2597
+ * - connecting - connection is being established
2598
+ * - connected - connection has been fully established
2599
+ * - disconnected - on requested disconnection or before reconnecting
2600
+ * - unavailable - after connection timeout or when there's no network
2601
+ *
2602
+ * Options:
2603
+ * - unavailableTimeout - time to transition to unavailable state
2604
+ * - activityTimeout - time after which ping message should be sent
2605
+ * - pongTimeout - time for Pusher to respond with pong before reconnecting
2606
+ *
2607
+ * @param {String} key application key
2608
+ * @param {Object} options
2609
+ */
2610
+ function ConnectionManager(key, options) {
2611
+ Pusher.EventsDispatcher.call(this);
669
2612
 
670
- if (self.connectedTimeout < MAX_CONNECTED_TIMEOUT) {
671
- self.connectedTimeout += CONNECTED_TIMEOUT_INCREMENT;
672
- }
2613
+ this.key = key;
2614
+ this.options = options || {};
2615
+ this.state = "initialized";
2616
+ this.connection = null;
2617
+ this.encrypted = !!options.encrypted;
2618
+ this.timeline = this.options.getTimeline();
673
2619
 
674
- // Toggle between ws & wss
675
- if (self.compulsorySecure !== true) {
676
- self.connectionSecure = !self.connectionSecure;
2620
+ var self = this;
2621
+
2622
+ Pusher.Network.bind("online", function() {
2623
+ if (self.state === "unavailable") {
2624
+ self.connect();
2625
+ }
2626
+ });
2627
+ Pusher.Network.bind("offline", function() {
2628
+ if (self.shouldRetry()) {
2629
+ self.disconnect();
2630
+ self.updateState("unavailable");
677
2631
  }
2632
+ });
678
2633
 
679
- self.failedAttempts++;
2634
+ var sendTimeline = function() {
2635
+ if (self.timelineSender) {
2636
+ self.timelineSender.send(function() {});
2637
+ }
2638
+ };
2639
+ this.bind("connected", sendTimeline);
2640
+ setInterval(sendTimeline, 60000);
2641
+ }
2642
+ var prototype = ConnectionManager.prototype;
2643
+
2644
+ Pusher.Util.extend(prototype, Pusher.EventsDispatcher.prototype);
2645
+
2646
+ /** Establishes a connection to Pusher.
2647
+ *
2648
+ * Does nothing when connection is already established. See top-level doc
2649
+ * to find events emitted on connection attempts.
2650
+ */
2651
+ prototype.connect = function() {
2652
+ if (this.connection) {
2653
+ return;
2654
+ }
2655
+ if (this.state === "connecting") {
2656
+ return;
680
2657
  }
681
2658
 
682
- function connectBaseURL(isSecure) {
683
- // Always connect with SSL if the current page served over https
684
- var ssl = (isSecure || document.location.protocol === 'https:');
685
- var port = ssl ? Pusher.wss_port : Pusher.ws_port;
686
- var scheme = ssl ? 'wss://' : 'ws://';
2659
+ var strategy = this.options.getStrategy({
2660
+ key: this.key,
2661
+ timeline: this.timeline,
2662
+ encrypted: this.encrypted
2663
+ });
687
2664
 
688
- return scheme + Pusher.host + ':' + port;
2665
+ if (!strategy.isSupported()) {
2666
+ this.updateState("failed");
2667
+ return;
689
2668
  }
690
-
691
- function connectPath(key) {
692
- var flash = (Pusher.TransportType === "flash") ? "true" : "false";
693
- var path = '/app/' + key + '?protocol=5&client=js'
694
- + '&version=' + Pusher.VERSION
695
- + '&flash=' + flash;
696
- return path;
2669
+ if (Pusher.Network.isOnline() === false) {
2670
+ this.updateState("unavailable");
2671
+ return;
697
2672
  }
698
2673
 
699
- function buildSockJSURL(isSecure) {
700
- var ssl = (isSecure || document.location.protocol === 'https:');
701
- var port = ssl ? Pusher.sockjs_https_port : Pusher.sockjs_http_port;
702
- var scheme = ssl ? 'https://' : 'http://';
2674
+ this.updateState("connecting");
2675
+ this.timelineSender = this.options.getTimelineSender(
2676
+ this.timeline,
2677
+ { encrypted: this.encrypted },
2678
+ this
2679
+ );
703
2680
 
704
- return scheme + Pusher.sockjs_host + ':' + port + Pusher.sockjs_path;
705
- }
2681
+ var self = this;
2682
+ var callback = function(error, transport) {
2683
+ if (error) {
2684
+ self.runner = strategy.connect(0, callback);
2685
+ } else {
2686
+ // we don't support switching connections yet
2687
+ self.runner.abort();
2688
+ self.setConnection(self.wrapTransport(transport));
2689
+ }
2690
+ };
2691
+ this.runner = strategy.connect(0, callback);
706
2692
 
707
- // callback for close and retry. Used on timeouts.
708
- function TransitionToImpermanentlyClosing() {
709
- self._machine.transition('impermanentlyClosing');
2693
+ this.setUnavailableTimer();
2694
+ };
2695
+
2696
+ /** Sends raw data.
2697
+ *
2698
+ * @param {String} data
2699
+ */
2700
+ prototype.send = function(data) {
2701
+ if (this.connection) {
2702
+ return this.connection.send(data);
2703
+ } else {
2704
+ return false;
710
2705
  }
2706
+ };
711
2707
 
712
- function resetActivityCheck() {
713
- if (self._activityTimer) { clearTimeout(self._activityTimer); }
714
- // Send ping after inactivity
715
- if (self.ping) {
716
- self._activityTimer = setTimeout(function() {
717
- self.send_event('pusher:ping', {})
718
- // Wait for pong response
719
- self._activityTimer = setTimeout(function() {
720
- self.socket.close();
721
- }, (self.options.pong_timeout || Pusher.pong_timeout))
722
- }, (self.options.activity_timeout || Pusher.activity_timeout))
723
- }
2708
+ /** Sends an event.
2709
+ *
2710
+ * @param {String} name
2711
+ * @param {String} data
2712
+ * @param {String} [channel]
2713
+ * @returns {Boolean} whether message was sent or not
2714
+ */
2715
+ prototype.send_event = function(name, data, channel) {
2716
+ if (this.connection) {
2717
+ return this.connection.send_event(name, data, channel);
2718
+ } else {
2719
+ return false;
724
2720
  }
2721
+ };
725
2722
 
726
- function stopActivityCheck() {
727
- if (self._activityTimer) { clearTimeout(self._activityTimer); }
2723
+ /** Closes the connection. */
2724
+ prototype.disconnect = function() {
2725
+ if (this.runner) {
2726
+ this.runner.abort();
2727
+ }
2728
+ this.clearRetryTimer();
2729
+ this.clearUnavailableTimer();
2730
+ this.stopActivityCheck();
2731
+ this.updateState("disconnected");
2732
+ // we're in disconnected state, so closing will not cause reconnecting
2733
+ if (this.connection) {
2734
+ this.connection.close();
2735
+ this.connection = null;
728
2736
  }
2737
+ };
729
2738
 
730
- // Returns the delay before the next connection attempt should be made
731
- //
732
- // This function guards against attempting to connect more frequently than
733
- // once every second
734
- //
735
- function connectionDelay() {
736
- var delay = self.connectionWait;
737
- if (delay === 0) {
738
- if (self.connectedAt) {
739
- var t = 1000;
740
- var connectedFor = new Date().getTime() - self.connectedAt;
741
- if (connectedFor < t) {
742
- delay = t - connectedFor;
743
- }
744
- }
745
- }
746
- return delay;
747
- }
748
-
749
- /*-----------------------------------------------
750
- WebSocket Callbacks
751
- -----------------------------------------------*/
752
-
753
- function handleCloseCode(code, message) {
754
- // first inform the end-developer of this error
755
- self.emit('error', {type: 'PusherError', data: {code: code, message: message}});
756
-
757
- if (code === 4000) {
758
- // SSL only app
759
- self.compulsorySecure = true;
760
- self.connectionSecure = true;
761
- self.options.encrypted = true;
762
-
763
- TransitionToImpermanentlyClosing();
764
- } else if (code < 4100) {
765
- // Permentently close connection
766
- self._machine.transition('permanentlyClosing')
767
- } else if (code < 4200) {
768
- // Backoff before reconnecting
769
- self.connectionWait = 1000;
770
- self._machine.transition('waiting')
771
- } else if (code < 4300) {
772
- // Reconnect immediately
773
- TransitionToImpermanentlyClosing();
774
- } else {
775
- // Unknown error
776
- self._machine.transition('permanentlyClosing')
2739
+ /** @private */
2740
+ prototype.retryIn = function(delay) {
2741
+ var self = this;
2742
+ this.retryTimer = setTimeout(function() {
2743
+ if (self.retryTimer === null) {
2744
+ return;
777
2745
  }
2746
+ self.retryTimer = null;
2747
+ self.disconnect();
2748
+ self.connect();
2749
+ }, delay || 0);
2750
+ };
2751
+
2752
+ /** @private */
2753
+ prototype.clearRetryTimer = function() {
2754
+ if (this.retryTimer) {
2755
+ clearTimeout(this.retryTimer);
2756
+ this.retryTimer = null;
778
2757
  }
2758
+ };
779
2759
 
780
- function ws_onMessageOpen(event) {
781
- var params = parseWebSocketEvent(event);
782
- if (params !== undefined) {
783
- if (params.event === 'pusher:connection_established') {
784
- self._machine.transition('connected', params.data.socket_id);
785
- } else if (params.event === 'pusher:error') {
786
- handleCloseCode(params.data.code, params.data.message)
787
- }
2760
+ /** @private */
2761
+ prototype.setUnavailableTimer = function() {
2762
+ var self = this;
2763
+ this.unavailableTimer = setTimeout(function() {
2764
+ if (!self.unavailableTimer) {
2765
+ return;
788
2766
  }
2767
+ self.updateState("unavailable");
2768
+ self.unavailableTimer = null;
2769
+ }, this.options.unavailableTimeout);
2770
+ };
2771
+
2772
+ /** @private */
2773
+ prototype.clearUnavailableTimer = function() {
2774
+ if (this.unavailableTimer) {
2775
+ clearTimeout(this.unavailableTimer);
2776
+ this.unavailableTimer = null;
789
2777
  }
2778
+ };
790
2779
 
791
- function ws_onMessageConnected(event) {
792
- resetActivityCheck();
2780
+ /** @private */
2781
+ prototype.resetActivityCheck = function() {
2782
+ this.stopActivityCheck();
2783
+ // send ping after inactivity
2784
+ if (!this.connection.supportsPing()) {
2785
+ var self = this;
2786
+ this.activityTimer = setTimeout(function() {
2787
+ self.send_event('pusher:ping', {});
2788
+ // wait for pong response
2789
+ self.activityTimer = setTimeout(function() {
2790
+ self.connection.close();
2791
+ }, (self.options.pongTimeout));
2792
+ }, (this.options.activityTimeout));
2793
+ }
2794
+ };
793
2795
 
794
- var params = parseWebSocketEvent(event);
795
- if (params !== undefined) {
796
- Pusher.debug('Event recd', params);
2796
+ /** @private */
2797
+ prototype.stopActivityCheck = function() {
2798
+ if (this.activityTimer) {
2799
+ clearTimeout(this.activityTimer);
2800
+ this.activityTimer = null;
2801
+ }
2802
+ };
797
2803
 
798
- switch (params.event) {
799
- case 'pusher:error':
800
- self.emit('error', {type: 'PusherError', data: params.data});
801
- break;
802
- case 'pusher:ping':
803
- self.send_event('pusher:pong', {})
804
- break;
805
- }
2804
+ /** @private */
2805
+ prototype.setConnection = function(connection) {
2806
+ this.connection = connection;
806
2807
 
807
- self.emit('message', params);
2808
+ var self = this;
2809
+ var onConnected = function(id) {
2810
+ self.clearUnavailableTimer();
2811
+ self.socket_id = id;
2812
+ self.updateState("connected");
2813
+ self.resetActivityCheck();
2814
+ };
2815
+ var onMessage = function(message) {
2816
+ // includes pong messages from server
2817
+ self.resetActivityCheck();
2818
+ self.emit('message', message);
2819
+ };
2820
+ var onPing = function() {
2821
+ self.send_event('pusher:pong', {});
2822
+ };
2823
+ var onPingRequest = function() {
2824
+ self.send_event('pusher:ping', {});
2825
+ };
2826
+ var onError = function(error) {
2827
+ // just emit error to user - socket will already be closed by browser
2828
+ self.emit("error", { type: "WebSocketError", error: error });
2829
+ };
2830
+ var onClosed = function() {
2831
+ connection.unbind("connected", onConnected);
2832
+ connection.unbind("message", onMessage);
2833
+ connection.unbind("ping", onPing);
2834
+ connection.unbind("ping_request", onPingRequest);
2835
+ connection.unbind("error", onError);
2836
+ connection.unbind("closed", onClosed);
2837
+ self.connection = null;
2838
+
2839
+ if (self.shouldRetry()) {
2840
+ self.retryIn(1000);
808
2841
  }
809
- }
2842
+ };
810
2843
 
2844
+ // handling close conditions
2845
+ var onSSLOnly = function() {
2846
+ self.encrypted = true;
2847
+ self.retryIn(0);
2848
+ };
2849
+ var onRefused = function() {
2850
+ self.disconnect();
2851
+ };
2852
+ var onBackoff = function() {
2853
+ self.retryIn(1000);
2854
+ };
2855
+ var onRetry = function() {
2856
+ self.retryIn(0);
2857
+ };
811
2858
 
812
- /**
813
- * Parses an event from the WebSocket to get
814
- * the JSON payload that we require
815
- *
816
- * @param {MessageEvent} event The event from the WebSocket.onmessage handler.
817
- **/
818
- function parseWebSocketEvent(event) {
819
- try {
820
- var params = JSON.parse(event.data);
821
-
822
- if (typeof params.data === 'string') {
823
- try {
824
- params.data = JSON.parse(params.data);
825
- } catch (e) {
826
- if (!(e instanceof SyntaxError)) {
827
- throw e;
828
- }
829
- }
830
- }
2859
+ connection.bind("connected", onConnected);
2860
+ connection.bind("message", onMessage);
2861
+ connection.bind("ping", onPing);
2862
+ connection.bind("ping_request", onPingRequest);
2863
+ connection.bind("error", onError);
2864
+ connection.bind("closed", onClosed);
831
2865
 
832
- return params;
833
- } catch (e) {
834
- self.emit('error', {type: 'MessageParseError', error: e, data: event.data});
835
- }
836
- }
2866
+ connection.bind("ssl_only", onSSLOnly);
2867
+ connection.bind("refused", onRefused);
2868
+ connection.bind("backoff", onBackoff);
2869
+ connection.bind("retry", onRetry);
837
2870
 
838
- function transitionToWaiting() {
839
- self._machine.transition('waiting');
840
- }
2871
+ this.resetActivityCheck();
2872
+ };
841
2873
 
842
- function ws_onError(error) {
843
- // just emit error to user - socket will already be closed by browser
844
- self.emit('error', { type: 'WebSocketError', error: error });
845
- }
2874
+ /** @private */
2875
+ prototype.updateState = function(newState, data) {
2876
+ var previousState = this.state;
846
2877
 
847
- // Updates the public state information exposed by connection
848
- //
849
- // This is distinct from the internal state information used by _machine
850
- // to manage the connection
851
- //
852
- function updateState(newState, data) {
853
- var prevState = self.state;
854
- self.state = newState;
2878
+ this.state = newState;
2879
+ // Only emit when the state changes
2880
+ if (previousState !== newState) {
2881
+ Pusher.debug('State changed', previousState + ' -> ' + newState);
855
2882
 
856
- // Only emit when the state changes
857
- if (prevState !== newState) {
858
- Pusher.debug('State changed', prevState + ' -> ' + newState);
859
- self.emit('state_change', {previous: prevState, current: newState});
860
- self.emit(newState, data);
861
- }
2883
+ this.emit('state_change', { previous: previousState, current: newState });
2884
+ this.emit(newState, data);
862
2885
  }
863
2886
  };
864
2887
 
865
- Connection.prototype.connect = function() {
866
- // no WebSockets
867
- if (!this._machine.is('failed') && !Pusher.Transport) {
868
- this._machine.transition('failed');
869
- }
870
- // initial open of connection
871
- else if(this._machine.is('initialized')) {
872
- resetConnectionParameters(this);
873
- this._machine.transition('waiting');
874
- }
875
- // user skipping connection wait
876
- else if (this._machine.is('waiting') && this.netInfo.isOnLine() === true) {
877
- this._machine.transition('connecting');
878
- }
879
- // user re-opening connection after closing it
880
- else if(this._machine.is("permanentlyClosed")) {
881
- resetConnectionParameters(this);
882
- this._machine.transition('waiting');
883
- }
2888
+ /** @private */
2889
+ prototype.shouldRetry = function() {
2890
+ return this.state === "connecting" || this.state === "connected";
884
2891
  };
885
2892
 
886
- Connection.prototype.send = function(data) {
887
- if (this._machine.is('connected')) {
888
- // Workaround for MobileSafari bug (see https://gist.github.com/2052006)
889
- var self = this;
890
- setTimeout(function() {
891
- self.socket.send(data);
892
- }, 0);
893
- return true;
894
- } else {
895
- return false;
896
- }
2893
+ /** @private */
2894
+ prototype.wrapTransport = function(transport) {
2895
+ return new Pusher.ProtocolWrapper(transport);
897
2896
  };
898
2897
 
899
- Connection.prototype.send_event = function(event_name, data, channel) {
900
- var payload = {
901
- event: event_name,
902
- data: data
903
- };
904
- if (channel) payload['channel'] = channel;
2898
+ Pusher.ConnectionManager = ConnectionManager;
2899
+ }).call(this);
905
2900
 
906
- Pusher.debug('Event sent', payload);
907
- return this.send(JSON.stringify(payload));
908
- }
2901
+ ;(function() {
2902
+ /** Really basic interface providing network availability info.
2903
+ *
2904
+ * Emits:
2905
+ * - online - when browser goes online
2906
+ * - offline - when browser goes offline
2907
+ */
2908
+ function NetInfo() {
2909
+ Pusher.EventsDispatcher.call(this);
909
2910
 
910
- Connection.prototype.disconnect = function() {
911
- if (this._machine.is('permanentlyClosed')) return;
2911
+ var self = this;
2912
+ // This is okay, as IE doesn't support this stuff anyway.
2913
+ if (window.addEventListener !== undefined) {
2914
+ window.addEventListener("online", function() {
2915
+ self.emit('online');
2916
+ }, false);
2917
+ window.addEventListener("offline", function() {
2918
+ self.emit('offline');
2919
+ }, false);
2920
+ }
2921
+ }
2922
+ Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
912
2923
 
913
- if (this._machine.is('waiting') || this._machine.is('failed')) {
914
- this._machine.transition('permanentlyClosed');
2924
+ var prototype = NetInfo.prototype;
2925
+
2926
+ /** Returns whether browser is online or not
2927
+ *
2928
+ * Offline means definitely offline (no connection to router).
2929
+ * Inverse does NOT mean definitely online (only currently supported in Safari
2930
+ * and even there only means the device has a connection to the router).
2931
+ *
2932
+ * @return {Boolean}
2933
+ */
2934
+ prototype.isOnline = function() {
2935
+ if (window.navigator.onLine === undefined) {
2936
+ return true;
915
2937
  } else {
916
- this._machine.transition('permanentlyClosing');
2938
+ return window.navigator.onLine;
917
2939
  }
918
2940
  };
919
2941
 
920
- Pusher.Util.extend(Connection.prototype, Pusher.EventsDispatcher.prototype);
921
- this.Pusher.Connection = Connection;
2942
+ Pusher.NetInfo = NetInfo;
2943
+ Pusher.Network = new NetInfo();
922
2944
  }).call(this);
923
2945
 
924
2946
  ;(function() {
@@ -1191,130 +3213,3 @@ Example:
1191
3213
  }
1192
3214
  };
1193
3215
  }).call(this);
1194
- // _require(dependencies, callback) takes an array of dependency urls and a
1195
- // callback to call when all the dependecies have finished loading
1196
- var _require = (function() {
1197
- function handleScriptLoaded(elem, callback) {
1198
- if (document.addEventListener) {
1199
- elem.addEventListener('load', callback, false);
1200
- } else {
1201
- elem.attachEvent('onreadystatechange', function () {
1202
- if (elem.readyState == 'loaded' || elem.readyState == 'complete') {
1203
- callback();
1204
- }
1205
- });
1206
- }
1207
- }
1208
-
1209
- function addScript(src, callback) {
1210
- var head = document.getElementsByTagName('head')[0];
1211
- var script = document.createElement('script');
1212
- script.setAttribute('src', src);
1213
- script.setAttribute("type","text/javascript");
1214
- script.setAttribute('async', true);
1215
-
1216
- handleScriptLoaded(script, function() {
1217
- callback();
1218
- });
1219
-
1220
- head.appendChild(script);
1221
- }
1222
-
1223
- return function(deps, callback) {
1224
- var deps_loaded = 0;
1225
- for (var i = 0; i < deps.length; i++) {
1226
- addScript(deps[i], function() {
1227
- if (deps.length == ++deps_loaded) {
1228
- // This setTimeout is a workaround for an Opera issue
1229
- setTimeout(callback, 0);
1230
- }
1231
- });
1232
- }
1233
- }
1234
- })();
1235
-
1236
- ;(function() {
1237
- // Support Firefox versions which prefix WebSocket
1238
- if (!window['WebSocket'] && window['MozWebSocket']) {
1239
- window['WebSocket'] = window['MozWebSocket']
1240
- }
1241
-
1242
- if (window['WebSocket']) {
1243
- Pusher.Transport = window['WebSocket'];
1244
- Pusher.TransportType = 'native';
1245
- }
1246
-
1247
- var cdn = (document.location.protocol == 'http:') ? Pusher.cdn_http : Pusher.cdn_https;
1248
- var root = cdn + Pusher.VERSION;
1249
- var deps = [];
1250
-
1251
- if (!window['JSON']) {
1252
- deps.push(root + '/json2' + Pusher.dependency_suffix + '.js');
1253
- }
1254
- if (!window['WebSocket']) {
1255
- var flashSupported;
1256
- try {
1257
- flashSupported = Boolean(new ActiveXObject('ShockwaveFlash.ShockwaveFlash'));
1258
- } catch (e) {
1259
- flashSupported = navigator.mimeTypes["application/x-shockwave-flash"] !== undefined;
1260
- }
1261
-
1262
- if (flashSupported) {
1263
- // Try to use web-socket-js (flash WebSocket emulation)
1264
- window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
1265
- window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
1266
- deps.push(root + '/flashfallback' + Pusher.dependency_suffix + '.js');
1267
- } else {
1268
- // Use SockJS when Flash is not available
1269
- deps.push(root + '/sockjs' + Pusher.dependency_suffix + '.js');
1270
- }
1271
- }
1272
-
1273
- var initialize = function() {
1274
- if (window['WebSocket']) {
1275
- // Initialize function in the case that we have native WebSocket support
1276
- return function() {
1277
- Pusher.ready();
1278
- }
1279
- } else {
1280
- // Initialize function for fallback case
1281
- return function() {
1282
- if (window['WebSocket']) {
1283
- // window['WebSocket'] is a flash emulation of WebSocket
1284
- Pusher.Transport = window['WebSocket'];
1285
- Pusher.TransportType = 'flash';
1286
-
1287
- window.WEB_SOCKET_SWF_LOCATION = root + "/WebSocketMain.swf";
1288
- WebSocket.__addTask(function() {
1289
- Pusher.ready();
1290
- })
1291
- WebSocket.__initialize();
1292
- } else {
1293
- // Flash fallback was not loaded, using SockJS
1294
- Pusher.Transport = window.SockJS;
1295
- Pusher.TransportType = 'sockjs';
1296
- Pusher.ready();
1297
- }
1298
- }
1299
- }
1300
- }();
1301
-
1302
- // Allows calling a function when the document body is available
1303
- var ondocumentbody = function(callback) {
1304
- var load_body = function() {
1305
- document.body ? callback() : setTimeout(load_body, 0);
1306
- }
1307
- load_body();
1308
- };
1309
-
1310
- var initializeOnDocumentBody = function() {
1311
- ondocumentbody(initialize);
1312
- }
1313
-
1314
- if (deps.length > 0) {
1315
- _require(deps, initializeOnDocumentBody);
1316
- } else {
1317
- initializeOnDocumentBody();
1318
- }
1319
- })();
1320
-