pusher_rails 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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
-