cometd-rails 0.0.1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +202 -0
  5. data/README.md +39 -0
  6. data/Rakefile +2 -0
  7. data/cometd-rails.gemspec +23 -0
  8. data/lib/cometd/rails/version.rb +5 -0
  9. data/lib/cometd/rails.rb +18 -0
  10. data/vendor/assets/javascripts/.DS_Store +0 -0
  11. data/vendor/assets/javascripts/cometd-dojox-rails.js +2 -0
  12. data/vendor/assets/javascripts/cometd-jquery-rails.js +2 -0
  13. data/vendor/assets/javascripts/common/.DS_Store +0 -0
  14. data/vendor/assets/javascripts/common/AckExtension.js +112 -0
  15. data/vendor/assets/javascripts/common/CallbackPollingTransport.js +166 -0
  16. data/vendor/assets/javascripts/common/CometD.js +2090 -0
  17. data/vendor/assets/javascripts/common/LongPollingTransport.js +111 -0
  18. data/vendor/assets/javascripts/common/ReloadExtension.js +254 -0
  19. data/vendor/assets/javascripts/common/RequestTransport.js +297 -0
  20. data/vendor/assets/javascripts/common/TimeStampExtension.js +42 -0
  21. data/vendor/assets/javascripts/common/TimeSyncExtension.js +216 -0
  22. data/vendor/assets/javascripts/common/Transport.js +142 -0
  23. data/vendor/assets/javascripts/common/TransportRegistry.js +116 -0
  24. data/vendor/assets/javascripts/common/Utils.js +58 -0
  25. data/vendor/assets/javascripts/common/WebSocketTransport.js +410 -0
  26. data/vendor/assets/javascripts/common/cometd-amd.js +7 -0
  27. data/vendor/assets/javascripts/common/cometd-header.js +15 -0
  28. data/vendor/assets/javascripts/common/cometd-json.js +5 -0
  29. data/vendor/assets/javascripts/common/cometd-namespace.js +3 -0
  30. data/vendor/assets/javascripts/common/cometd.require.js +16 -0
  31. data/vendor/assets/javascripts/dojox/.DS_Store +0 -0
  32. data/vendor/assets/javascripts/dojox/ack.js +22 -0
  33. data/vendor/assets/javascripts/dojox/dojox.require.js +5 -0
  34. data/vendor/assets/javascripts/dojox/main.js +95 -0
  35. data/vendor/assets/javascripts/dojox/reload.js +26 -0
  36. data/vendor/assets/javascripts/dojox/timestamp.js +22 -0
  37. data/vendor/assets/javascripts/dojox/timesync.js +22 -0
  38. data/vendor/assets/javascripts/jquery/.DS_Store +0 -0
  39. data/vendor/assets/javascripts/jquery/jquery.cometd-ack.js +33 -0
  40. data/vendor/assets/javascripts/jquery/jquery.cometd-reload.js +41 -0
  41. data/vendor/assets/javascripts/jquery/jquery.cometd-timestamp.js +33 -0
  42. data/vendor/assets/javascripts/jquery/jquery.cometd-timesync.js +33 -0
  43. data/vendor/assets/javascripts/jquery/jquery.cometd.js +140 -0
  44. data/vendor/assets/javascripts/jquery/jquery.cometd.require.js +5 -0
  45. metadata +115 -0
@@ -0,0 +1,2090 @@
1
+ /**
2
+ * The constructor for a CometD object, identified by an optional name.
3
+ * The default name is the string 'default'.
4
+ * In the rare case a page needs more than one Bayeux conversation,
5
+ * a new instance can be created via:
6
+ * <pre>
7
+ * var bayeuxUrl2 = ...;
8
+ *
9
+ * // Dojo style
10
+ * var cometd2 = new dojox.CometD('another_optional_name');
11
+ *
12
+ * // jQuery style
13
+ * var cometd2 = new $.CometD('another_optional_name');
14
+ *
15
+ * cometd2.init({url: bayeuxUrl2});
16
+ * </pre>
17
+ * @param name the optional name of this cometd object
18
+ */
19
+ // IMPLEMENTATION NOTES:
20
+ // Be very careful in not changing the function order and pass this file every time through JSLint (http://jslint.com)
21
+ // The only implied globals must be "dojo", "org" and "window", and check that there are no "unused" warnings
22
+ // Failing to pass JSLint may result in shrinkers/minifiers to create an unusable file.
23
+ org.cometd.CometD = function(name)
24
+ {
25
+ var _cometd = this;
26
+ var _name = name || 'default';
27
+ var _crossDomain = false;
28
+ var _transports = new org.cometd.TransportRegistry();
29
+ var _transport;
30
+ var _status = 'disconnected';
31
+ var _messageId = 0;
32
+ var _clientId = null;
33
+ var _batch = 0;
34
+ var _messageQueue = [];
35
+ var _internalBatch = false;
36
+ var _listeners = {};
37
+ var _backoff = 0;
38
+ var _scheduledSend = null;
39
+ var _extensions = [];
40
+ var _advice = {};
41
+ var _handshakeProps;
42
+ var _handshakeCallback;
43
+ var _callbacks = {};
44
+ var _remoteCalls = {};
45
+ var _reestablish = false;
46
+ var _connected = false;
47
+ var _config = {
48
+ protocol: null,
49
+ stickyReconnect: true,
50
+ connectTimeout: 0,
51
+ maxConnections: 2,
52
+ backoffIncrement: 1000,
53
+ maxBackoff: 60000,
54
+ logLevel: 'info',
55
+ reverseIncomingExtensions: true,
56
+ maxNetworkDelay: 10000,
57
+ requestHeaders: {},
58
+ appendMessageTypeToURL: true,
59
+ autoBatch: false,
60
+ advice: {
61
+ timeout: 60000,
62
+ interval: 0,
63
+ reconnect: 'retry'
64
+ }
65
+ };
66
+
67
+ function _fieldValue(object, name)
68
+ {
69
+ try
70
+ {
71
+ return object[name];
72
+ }
73
+ catch (x)
74
+ {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Mixes in the given objects into the target object by copying the properties.
81
+ * @param deep if the copy must be deep
82
+ * @param target the target object
83
+ * @param objects the objects whose properties are copied into the target
84
+ */
85
+ this._mixin = function(deep, target, objects)
86
+ {
87
+ var result = target || {};
88
+
89
+ // Skip first 2 parameters (deep and target), and loop over the others
90
+ for (var i = 2; i < arguments.length; ++i)
91
+ {
92
+ var object = arguments[i];
93
+
94
+ if (object === undefined || object === null)
95
+ {
96
+ continue;
97
+ }
98
+
99
+ for (var propName in object)
100
+ {
101
+ var prop = _fieldValue(object, propName);
102
+ var targ = _fieldValue(result, propName);
103
+
104
+ // Avoid infinite loops
105
+ if (prop === target)
106
+ {
107
+ continue;
108
+ }
109
+ // Do not mixin undefined values
110
+ if (prop === undefined)
111
+ {
112
+ continue;
113
+ }
114
+
115
+ if (deep && typeof prop === 'object' && prop !== null)
116
+ {
117
+ if (prop instanceof Array)
118
+ {
119
+ result[propName] = this._mixin(deep, targ instanceof Array ? targ : [], prop);
120
+ }
121
+ else
122
+ {
123
+ var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {};
124
+ result[propName] = this._mixin(deep, source, prop);
125
+ }
126
+ }
127
+ else
128
+ {
129
+ result[propName] = prop;
130
+ }
131
+ }
132
+ }
133
+
134
+ return result;
135
+ };
136
+
137
+ function _isString(value)
138
+ {
139
+ return org.cometd.Utils.isString(value);
140
+ }
141
+
142
+ function _isFunction(value)
143
+ {
144
+ if (value === undefined || value === null)
145
+ {
146
+ return false;
147
+ }
148
+ return typeof value === 'function';
149
+ }
150
+
151
+ function _log(level, args)
152
+ {
153
+ if (window.console)
154
+ {
155
+ var logger = window.console[level];
156
+ if (_isFunction(logger))
157
+ {
158
+ logger.apply(window.console, args);
159
+ }
160
+ }
161
+ }
162
+
163
+ this._warn = function()
164
+ {
165
+ _log('warn', arguments);
166
+ };
167
+
168
+ this._info = function()
169
+ {
170
+ if (_config.logLevel !== 'warn')
171
+ {
172
+ _log('info', arguments);
173
+ }
174
+ };
175
+
176
+ this._debug = function()
177
+ {
178
+ if (_config.logLevel === 'debug')
179
+ {
180
+ _log('debug', arguments);
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Returns whether the given hostAndPort is cross domain.
186
+ * The default implementation checks against window.location.host
187
+ * but this function can be overridden to make it work in non-browser
188
+ * environments.
189
+ *
190
+ * @param hostAndPort the host and port in format host:port
191
+ * @return whether the given hostAndPort is cross domain
192
+ */
193
+ this._isCrossDomain = function(hostAndPort)
194
+ {
195
+ return hostAndPort && hostAndPort !== window.location.host;
196
+ };
197
+
198
+ function _configure(configuration)
199
+ {
200
+ _cometd._debug('Configuring cometd object with', configuration);
201
+ // Support old style param, where only the Bayeux server URL was passed
202
+ if (_isString(configuration))
203
+ {
204
+ configuration = { url: configuration };
205
+ }
206
+ if (!configuration)
207
+ {
208
+ configuration = {};
209
+ }
210
+
211
+ _config = _cometd._mixin(false, _config, configuration);
212
+
213
+ var url = _cometd.getURL();
214
+ if (!url)
215
+ {
216
+ throw 'Missing required configuration parameter \'url\' specifying the Bayeux server URL';
217
+ }
218
+
219
+ // Check if we're cross domain
220
+ // [1] = protocol://, [2] = host:port, [3] = host, [4] = IPv6_host, [5] = IPv4_host, [6] = :port, [7] = port, [8] = uri, [9] = rest
221
+ var urlParts = /(^https?:\/\/)?(((\[[^\]]+\])|([^:\/\?#]+))(:(\d+))?)?([^\?#]*)(.*)?/.exec(url);
222
+ var hostAndPort = urlParts[2];
223
+ var uri = urlParts[8];
224
+ var afterURI = urlParts[9];
225
+ _crossDomain = _cometd._isCrossDomain(hostAndPort);
226
+
227
+ // Check if appending extra path is supported
228
+ if (_config.appendMessageTypeToURL)
229
+ {
230
+ if (afterURI !== undefined && afterURI.length > 0)
231
+ {
232
+ _cometd._info('Appending message type to URI ' + uri + afterURI + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
233
+ _config.appendMessageTypeToURL = false;
234
+ }
235
+ else
236
+ {
237
+ var uriSegments = uri.split('/');
238
+ var lastSegmentIndex = uriSegments.length - 1;
239
+ if (uri.match(/\/$/))
240
+ {
241
+ lastSegmentIndex -= 1;
242
+ }
243
+ if (uriSegments[lastSegmentIndex].indexOf('.') >= 0)
244
+ {
245
+ // Very likely the CometD servlet's URL pattern is mapped to an extension, such as *.cometd
246
+ // It will be difficult to add the extra path in this case
247
+ _cometd._info('Appending message type to URI ' + uri + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
248
+ _config.appendMessageTypeToURL = false;
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ function _removeListener(subscription)
255
+ {
256
+ if (subscription)
257
+ {
258
+ var subscriptions = _listeners[subscription.channel];
259
+ if (subscriptions && subscriptions[subscription.id])
260
+ {
261
+ delete subscriptions[subscription.id];
262
+ _cometd._debug('Removed', subscription.listener ? 'listener' : 'subscription', subscription);
263
+ }
264
+ }
265
+ }
266
+
267
+ function _removeSubscription(subscription)
268
+ {
269
+ if (subscription && !subscription.listener)
270
+ {
271
+ _removeListener(subscription);
272
+ }
273
+ }
274
+
275
+ function _clearSubscriptions()
276
+ {
277
+ for (var channel in _listeners)
278
+ {
279
+ var subscriptions = _listeners[channel];
280
+ if (subscriptions)
281
+ {
282
+ for (var i = 0; i < subscriptions.length; ++i)
283
+ {
284
+ _removeSubscription(subscriptions[i]);
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ function _setStatus(newStatus)
291
+ {
292
+ if (_status !== newStatus)
293
+ {
294
+ _cometd._debug('Status', _status, '->', newStatus);
295
+ _status = newStatus;
296
+ }
297
+ }
298
+
299
+ function _isDisconnected()
300
+ {
301
+ return _status === 'disconnecting' || _status === 'disconnected';
302
+ }
303
+
304
+ function _nextMessageId()
305
+ {
306
+ var result = ++_messageId;
307
+ return '' + result;
308
+ }
309
+
310
+ function _applyExtension(scope, callback, name, message, outgoing)
311
+ {
312
+ try
313
+ {
314
+ return callback.call(scope, message);
315
+ }
316
+ catch (x)
317
+ {
318
+ var handler = _cometd.onExtensionException;
319
+ if (_isFunction(handler))
320
+ {
321
+ _cometd._debug('Invoking extension exception handler', name, x);
322
+ try
323
+ {
324
+ handler.call(_cometd, x, name, outgoing, message);
325
+ }
326
+ catch(xx)
327
+ {
328
+ _cometd._info('Exception during execution of extension exception handler', name, xx);
329
+ }
330
+ }
331
+ else
332
+ {
333
+ _cometd._info('Exception during execution of extension', name, x);
334
+ }
335
+ return message;
336
+ }
337
+ }
338
+
339
+ function _applyIncomingExtensions(message)
340
+ {
341
+ for (var i = 0; i < _extensions.length; ++i)
342
+ {
343
+ if (message === undefined || message === null)
344
+ {
345
+ break;
346
+ }
347
+
348
+ var index = _config.reverseIncomingExtensions ? _extensions.length - 1 - i : i;
349
+ var extension = _extensions[index];
350
+ var callback = extension.extension.incoming;
351
+ if (_isFunction(callback))
352
+ {
353
+ var result = _applyExtension(extension.extension, callback, extension.name, message, false);
354
+ message = result === undefined ? message : result;
355
+ }
356
+ }
357
+ return message;
358
+ }
359
+
360
+ function _applyOutgoingExtensions(message)
361
+ {
362
+ for (var i = 0; i < _extensions.length; ++i)
363
+ {
364
+ if (message === undefined || message === null)
365
+ {
366
+ break;
367
+ }
368
+
369
+ var extension = _extensions[i];
370
+ var callback = extension.extension.outgoing;
371
+ if (_isFunction(callback))
372
+ {
373
+ var result = _applyExtension(extension.extension, callback, extension.name, message, true);
374
+ message = result === undefined ? message : result;
375
+ }
376
+ }
377
+ return message;
378
+ }
379
+
380
+ function _notify(channel, message)
381
+ {
382
+ var subscriptions = _listeners[channel];
383
+ if (subscriptions && subscriptions.length > 0)
384
+ {
385
+ for (var i = 0; i < subscriptions.length; ++i)
386
+ {
387
+ var subscription = subscriptions[i];
388
+ // Subscriptions may come and go, so the array may have 'holes'
389
+ if (subscription)
390
+ {
391
+ try
392
+ {
393
+ subscription.callback.call(subscription.scope, message);
394
+ }
395
+ catch (x)
396
+ {
397
+ var handler = _cometd.onListenerException;
398
+ if (_isFunction(handler))
399
+ {
400
+ _cometd._debug('Invoking listener exception handler', subscription, x);
401
+ try
402
+ {
403
+ handler.call(_cometd, x, subscription, subscription.listener, message);
404
+ }
405
+ catch (xx)
406
+ {
407
+ _cometd._info('Exception during execution of listener exception handler', subscription, xx);
408
+ }
409
+ }
410
+ else
411
+ {
412
+ _cometd._info('Exception during execution of listener', subscription, message, x);
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ function _notifyListeners(channel, message)
421
+ {
422
+ // Notify direct listeners
423
+ _notify(channel, message);
424
+
425
+ // Notify the globbing listeners
426
+ var channelParts = channel.split('/');
427
+ var last = channelParts.length - 1;
428
+ for (var i = last; i > 0; --i)
429
+ {
430
+ var channelPart = channelParts.slice(0, i).join('/') + '/*';
431
+ // We don't want to notify /foo/* if the channel is /foo/bar/baz,
432
+ // so we stop at the first non recursive globbing
433
+ if (i === last)
434
+ {
435
+ _notify(channelPart, message);
436
+ }
437
+ // Add the recursive globber and notify
438
+ channelPart += '*';
439
+ _notify(channelPart, message);
440
+ }
441
+ }
442
+
443
+ function _cancelDelayedSend()
444
+ {
445
+ if (_scheduledSend !== null)
446
+ {
447
+ org.cometd.Utils.clearTimeout(_scheduledSend);
448
+ }
449
+ _scheduledSend = null;
450
+ }
451
+
452
+ function _delayedSend(operation)
453
+ {
454
+ _cancelDelayedSend();
455
+ var delay = _advice.interval + _backoff;
456
+ _cometd._debug('Function scheduled in', delay, 'ms, interval =', _advice.interval, 'backoff =', _backoff, operation);
457
+ _scheduledSend = org.cometd.Utils.setTimeout(_cometd, operation, delay);
458
+ }
459
+
460
+ // Needed to break cyclic dependencies between function definitions
461
+ var _handleMessages;
462
+ var _handleFailure;
463
+
464
+ /**
465
+ * Delivers the messages to the CometD server
466
+ * @param sync whether the send is synchronous
467
+ * @param messages the array of messages to send
468
+ * @param metaConnect true if this send is on /meta/connect
469
+ * @param extraPath an extra path to append to the Bayeux server URL
470
+ */
471
+ function _send(sync, messages, metaConnect, extraPath)
472
+ {
473
+ // We must be sure that the messages have a clientId.
474
+ // This is not guaranteed since the handshake may take time to return
475
+ // (and hence the clientId is not known yet) and the application
476
+ // may create other messages.
477
+ for (var i = 0; i < messages.length; ++i)
478
+ {
479
+ var message = messages[i];
480
+ var messageId = message.id;
481
+
482
+ if (_clientId)
483
+ {
484
+ message.clientId = _clientId;
485
+ }
486
+
487
+ var callback = undefined;
488
+ if (_isFunction(message._callback))
489
+ {
490
+ callback = message._callback;
491
+ // Remove the callback before calling the extensions.
492
+ delete message._callback;
493
+ }
494
+
495
+ message = _applyOutgoingExtensions(message);
496
+ if (message !== undefined && message !== null)
497
+ {
498
+ // Extensions may have modified the message id, but we need to own it.
499
+ message.id = messageId;
500
+ messages[i] = message;
501
+ if (callback !== undefined)
502
+ {
503
+ _callbacks[messageId] = callback;
504
+ }
505
+ }
506
+ else
507
+ {
508
+ messages.splice(i--, 1);
509
+ }
510
+ }
511
+
512
+ if (messages.length === 0)
513
+ {
514
+ return;
515
+ }
516
+
517
+ var url = _cometd.getURL();
518
+ if (_config.appendMessageTypeToURL)
519
+ {
520
+ // If url does not end with '/', then append it
521
+ if (!url.match(/\/$/))
522
+ {
523
+ url = url + '/';
524
+ }
525
+ if (extraPath)
526
+ {
527
+ url = url + extraPath;
528
+ }
529
+ }
530
+
531
+ var envelope = {
532
+ url: url,
533
+ sync: sync,
534
+ messages: messages,
535
+ onSuccess: function(rcvdMessages)
536
+ {
537
+ try
538
+ {
539
+ _handleMessages.call(_cometd, rcvdMessages);
540
+ }
541
+ catch (x)
542
+ {
543
+ _cometd._info('Exception during handling of messages', x);
544
+ }
545
+ },
546
+ onFailure: function(conduit, messages, failure)
547
+ {
548
+ try
549
+ {
550
+ var transport = _cometd.getTransport();
551
+ failure.connectionType = transport ? transport.getType() : "unknown";
552
+ _handleFailure.call(_cometd, conduit, messages, failure);
553
+ }
554
+ catch (x)
555
+ {
556
+ _cometd._info('Exception during handling of failure', x);
557
+ }
558
+ }
559
+ };
560
+ _cometd._debug('Send', envelope);
561
+ _transport.send(envelope, metaConnect);
562
+ }
563
+
564
+ function _queueSend(message)
565
+ {
566
+ if (_batch > 0 || _internalBatch === true)
567
+ {
568
+ _messageQueue.push(message);
569
+ }
570
+ else
571
+ {
572
+ _send(false, [message], false);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Sends a complete bayeux message.
578
+ * This method is exposed as a public so that extensions may use it
579
+ * to send bayeux message directly, for example in case of re-sending
580
+ * messages that have already been sent but that for some reason must
581
+ * be resent.
582
+ */
583
+ this.send = _queueSend;
584
+
585
+ function _resetBackoff()
586
+ {
587
+ _backoff = 0;
588
+ }
589
+
590
+ function _increaseBackoff()
591
+ {
592
+ if (_backoff < _config.maxBackoff)
593
+ {
594
+ _backoff += _config.backoffIncrement;
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Starts a the batch of messages to be sent in a single request.
600
+ * @see #_endBatch(sendMessages)
601
+ */
602
+ function _startBatch()
603
+ {
604
+ ++_batch;
605
+ }
606
+
607
+ function _flushBatch()
608
+ {
609
+ var messages = _messageQueue;
610
+ _messageQueue = [];
611
+ if (messages.length > 0)
612
+ {
613
+ _send(false, messages, false);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Ends the batch of messages to be sent in a single request,
619
+ * optionally sending messages present in the message queue depending
620
+ * on the given argument.
621
+ * @see #_startBatch()
622
+ */
623
+ function _endBatch()
624
+ {
625
+ --_batch;
626
+ if (_batch < 0)
627
+ {
628
+ throw 'Calls to startBatch() and endBatch() are not paired';
629
+ }
630
+
631
+ if (_batch === 0 && !_isDisconnected() && !_internalBatch)
632
+ {
633
+ _flushBatch();
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Sends the connect message
639
+ */
640
+ function _connect()
641
+ {
642
+ if (!_isDisconnected())
643
+ {
644
+ var bayeuxMessage = {
645
+ id: _nextMessageId(),
646
+ channel: '/meta/connect',
647
+ connectionType: _transport.getType()
648
+ };
649
+
650
+ // In case of reload or temporary loss of connection
651
+ // we want the next successful connect to return immediately
652
+ // instead of being held by the server, so that connect listeners
653
+ // can be notified that the connection has been re-established
654
+ if (!_connected)
655
+ {
656
+ bayeuxMessage.advice = { timeout: 0 };
657
+ }
658
+
659
+ _setStatus('connecting');
660
+ _cometd._debug('Connect sent', bayeuxMessage);
661
+ _send(false, [bayeuxMessage], true, 'connect');
662
+ _setStatus('connected');
663
+ }
664
+ }
665
+
666
+ function _delayedConnect()
667
+ {
668
+ _setStatus('connecting');
669
+ _delayedSend(function()
670
+ {
671
+ _connect();
672
+ });
673
+ }
674
+
675
+ function _updateAdvice(newAdvice)
676
+ {
677
+ if (newAdvice)
678
+ {
679
+ _advice = _cometd._mixin(false, {}, _config.advice, newAdvice);
680
+ _cometd._debug('New advice', _advice);
681
+ }
682
+ }
683
+
684
+ function _disconnect(abort)
685
+ {
686
+ _cancelDelayedSend();
687
+ if (abort && _transport)
688
+ {
689
+ _transport.abort();
690
+ }
691
+ _clientId = null;
692
+ _setStatus('disconnected');
693
+ _batch = 0;
694
+ _resetBackoff();
695
+ _transport = null;
696
+
697
+ // Fail any existing queued message
698
+ if (_messageQueue.length > 0)
699
+ {
700
+ var messages = _messageQueue;
701
+ _messageQueue = [];
702
+ _handleFailure.call(_cometd, undefined, messages, {
703
+ reason: 'Disconnected'
704
+ });
705
+ }
706
+ }
707
+
708
+ function _notifyTransportFailure(oldTransport, newTransport, failure)
709
+ {
710
+ var handler = _cometd.onTransportException;
711
+ if (_isFunction(handler))
712
+ {
713
+ _cometd._debug('Invoking transport exception handler', oldTransport, newTransport, failure);
714
+ try
715
+ {
716
+ handler.call(_cometd, failure, oldTransport, newTransport);
717
+ }
718
+ catch (x)
719
+ {
720
+ _cometd._info('Exception during execution of transport exception handler', x);
721
+ }
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Sends the initial handshake message
727
+ */
728
+ function _handshake(handshakeProps, handshakeCallback)
729
+ {
730
+ if (_isFunction(handshakeProps))
731
+ {
732
+ handshakeCallback = handshakeProps;
733
+ handshakeProps = undefined;
734
+ }
735
+
736
+ _clientId = null;
737
+
738
+ _clearSubscriptions();
739
+
740
+ // Reset the transports if we're not retrying the handshake
741
+ if (_isDisconnected())
742
+ {
743
+ _transports.reset();
744
+ _updateAdvice(_config.advice);
745
+ }
746
+ else
747
+ {
748
+ // We are retrying the handshake, either because another handshake failed
749
+ // and we're backing off, or because the server timed us out and asks us to
750
+ // re-handshake: in both cases, make sure that if the handshake succeeds
751
+ // the next action is a connect.
752
+ _updateAdvice(_cometd._mixin(false, _advice, {reconnect: 'retry'}));
753
+ }
754
+
755
+ _batch = 0;
756
+
757
+ // Mark the start of an internal batch.
758
+ // This is needed because handshake and connect are async.
759
+ // It may happen that the application calls init() then subscribe()
760
+ // and the subscribe message is sent before the connect message, if
761
+ // the subscribe message is not held until the connect message is sent.
762
+ // So here we start a batch to hold temporarily any message until
763
+ // the connection is fully established.
764
+ _internalBatch = true;
765
+
766
+ // Save the properties provided by the user, so that
767
+ // we can reuse them during automatic re-handshake
768
+ _handshakeProps = handshakeProps;
769
+ _handshakeCallback = handshakeCallback;
770
+
771
+ var version = '1.0';
772
+
773
+ // Figure out the transports to send to the server
774
+ var url = _cometd.getURL();
775
+ var transportTypes = _transports.findTransportTypes(version, _crossDomain, url);
776
+
777
+ var bayeuxMessage = {
778
+ id: _nextMessageId(),
779
+ version: version,
780
+ minimumVersion: version,
781
+ channel: '/meta/handshake',
782
+ supportedConnectionTypes: transportTypes,
783
+ _callback: handshakeCallback,
784
+ advice: {
785
+ timeout: _advice.timeout,
786
+ interval: _advice.interval
787
+ }
788
+ };
789
+ // Do not allow the user to override important fields.
790
+ var message = _cometd._mixin(false, {}, _handshakeProps, bayeuxMessage);
791
+
792
+ // Pick up the first available transport as initial transport
793
+ // since we don't know if the server supports it
794
+ if (!_transport)
795
+ {
796
+ _transport = _transports.negotiateTransport(transportTypes, version, _crossDomain, url);
797
+ if (!_transport)
798
+ {
799
+ var failure = 'Could not find initial transport among: ' + _transports.getTransportTypes();
800
+ _cometd._warn(failure);
801
+ throw failure;
802
+ }
803
+ }
804
+
805
+ _cometd._debug('Initial transport is', _transport.getType());
806
+
807
+ // We started a batch to hold the application messages,
808
+ // so here we must bypass it and send immediately.
809
+ _setStatus('handshaking');
810
+ _cometd._debug('Handshake sent', message);
811
+ _send(false, [message], false, 'handshake');
812
+ }
813
+
814
+ function _delayedHandshake()
815
+ {
816
+ _setStatus('handshaking');
817
+
818
+ // We will call _handshake() which will reset _clientId, but we want to avoid
819
+ // that between the end of this method and the call to _handshake() someone may
820
+ // call publish() (or other methods that call _queueSend()).
821
+ _internalBatch = true;
822
+
823
+ _delayedSend(function()
824
+ {
825
+ _handshake(_handshakeProps, _handshakeCallback);
826
+ });
827
+ }
828
+
829
+ function _notifyCallback(callback, message)
830
+ {
831
+ try
832
+ {
833
+ callback.call(_cometd, message);
834
+ }
835
+ catch (x)
836
+ {
837
+ var handler = _cometd.onCallbackException;
838
+ if (_isFunction(handler))
839
+ {
840
+ _cometd._debug('Invoking callback exception handler', x);
841
+ try
842
+ {
843
+ handler.call(_cometd, x, message);
844
+ }
845
+ catch (xx)
846
+ {
847
+ _cometd._info('Exception during execution of callback exception handler', xx);
848
+ }
849
+ }
850
+ else
851
+ {
852
+ _cometd._info('Exception during execution of message callback', x);
853
+ }
854
+ }
855
+ }
856
+
857
+ function _handleCallback(message)
858
+ {
859
+ var callback = _callbacks[message.id];
860
+ if (_isFunction(callback))
861
+ {
862
+ delete _callbacks[message.id];
863
+ _notifyCallback(callback, message);
864
+ }
865
+ }
866
+
867
+ function _handleRemoteCall(message)
868
+ {
869
+ var context = _remoteCalls[message.id];
870
+ delete _remoteCalls[message.id];
871
+ _cometd._debug('Handling remote call response for', message, 'with context', context);
872
+ if (context)
873
+ {
874
+ // Clear the timeout, if present.
875
+ var timeout = context.timeout;
876
+ if (timeout)
877
+ {
878
+ org.cometd.Utils.clearTimeout(timeout);
879
+ }
880
+
881
+ var callback = context.callback;
882
+ if (_isFunction(callback))
883
+ {
884
+ _notifyCallback(callback, message);
885
+ return true;
886
+ }
887
+ }
888
+ return false;
889
+ }
890
+
891
+ function _failHandshake(message)
892
+ {
893
+ _handleCallback(message);
894
+ _notifyListeners('/meta/handshake', message);
895
+ _notifyListeners('/meta/unsuccessful', message);
896
+
897
+ // Only try again if we haven't been disconnected and
898
+ // the advice permits us to retry the handshake
899
+ var retry = !_isDisconnected() && _advice.reconnect !== 'none';
900
+ if (retry)
901
+ {
902
+ _increaseBackoff();
903
+ _delayedHandshake();
904
+ }
905
+ else
906
+ {
907
+ _disconnect(true);
908
+ }
909
+ }
910
+
911
+ function _handshakeResponse(message)
912
+ {
913
+ if (message.successful)
914
+ {
915
+ // Save clientId, figure out transport, then follow the advice to connect
916
+ _clientId = message.clientId;
917
+
918
+ var url = _cometd.getURL();
919
+ var newTransport = _transports.negotiateTransport(message.supportedConnectionTypes, message.version, _crossDomain, url);
920
+ if (newTransport === null)
921
+ {
922
+ var failure = 'Could not negotiate transport with server; client=[' +
923
+ _transports.findTransportTypes(message.version, _crossDomain, url) +
924
+ '], server=[' + message.supportedConnectionTypes + ']';
925
+ var oldTransport = _cometd.getTransport();
926
+ _notifyTransportFailure(oldTransport.getType(), null, {
927
+ reason: failure,
928
+ connectionType: oldTransport.getType(),
929
+ transport: oldTransport
930
+ });
931
+ _cometd._warn(failure);
932
+ _disconnect(true);
933
+ return;
934
+ }
935
+ else if (_transport !== newTransport)
936
+ {
937
+ _cometd._debug('Transport', _transport.getType(), '->', newTransport.getType());
938
+ _transport = newTransport;
939
+ }
940
+
941
+ // End the internal batch and allow held messages from the application
942
+ // to go to the server (see _handshake() where we start the internal batch).
943
+ _internalBatch = false;
944
+ _flushBatch();
945
+
946
+ // Here the new transport is in place, as well as the clientId, so
947
+ // the listeners can perform a publish() if they want.
948
+ // Notify the listeners before the connect below.
949
+ message.reestablish = _reestablish;
950
+ _reestablish = true;
951
+
952
+ _handleCallback(message);
953
+ _notifyListeners('/meta/handshake', message);
954
+
955
+ var action = _isDisconnected() ? 'none' : _advice.reconnect;
956
+ switch (action)
957
+ {
958
+ case 'retry':
959
+ _resetBackoff();
960
+ _delayedConnect();
961
+ break;
962
+ case 'none':
963
+ _disconnect(true);
964
+ break;
965
+ default:
966
+ throw 'Unrecognized advice action ' + action;
967
+ }
968
+ }
969
+ else
970
+ {
971
+ _failHandshake(message);
972
+ }
973
+ }
974
+
975
+ function _handshakeFailure(message)
976
+ {
977
+ var version = '1.0';
978
+ var url = _cometd.getURL();
979
+ var oldTransport = _cometd.getTransport();
980
+ var transportTypes = _transports.findTransportTypes(version, _crossDomain, url);
981
+ var newTransport = _transports.negotiateTransport(transportTypes, version, _crossDomain, url);
982
+ if (!newTransport)
983
+ {
984
+ _notifyTransportFailure(oldTransport.getType(), null, message.failure);
985
+ _cometd._warn('Could not negotiate transport; client=[' + transportTypes + ']');
986
+ _disconnect(true);
987
+ _failHandshake(message);
988
+ }
989
+ else
990
+ {
991
+ _cometd._debug('Transport', oldTransport.getType(), '->', newTransport.getType());
992
+ _notifyTransportFailure(oldTransport.getType(), newTransport.getType(), message.failure);
993
+ _failHandshake(message);
994
+ _transport = newTransport;
995
+ }
996
+ }
997
+
998
+ function _failConnect(message)
999
+ {
1000
+ // Notify the listeners after the status change but before the next action
1001
+ _notifyListeners('/meta/connect', message);
1002
+ _notifyListeners('/meta/unsuccessful', message);
1003
+
1004
+ // This may happen when the server crashed, the current clientId
1005
+ // will be invalid, and the server will ask to handshake again
1006
+ // Listeners can call disconnect(), so check the state after they run
1007
+ var action = _isDisconnected() ? 'none' : _advice.reconnect;
1008
+ switch (action)
1009
+ {
1010
+ case 'retry':
1011
+ _delayedConnect();
1012
+ _increaseBackoff();
1013
+ break;
1014
+ case 'handshake':
1015
+ // The current transport may be failed (e.g. network disconnection)
1016
+ // Reset the transports so the new handshake picks up the right one
1017
+ _transports.reset();
1018
+ _resetBackoff();
1019
+ _delayedHandshake();
1020
+ break;
1021
+ case 'none':
1022
+ _disconnect(true);
1023
+ break;
1024
+ default:
1025
+ throw 'Unrecognized advice action' + action;
1026
+ }
1027
+ }
1028
+
1029
+ function _connectResponse(message)
1030
+ {
1031
+ _connected = message.successful;
1032
+
1033
+ if (_connected)
1034
+ {
1035
+ _notifyListeners('/meta/connect', message);
1036
+
1037
+ // Normally, the advice will say "reconnect: 'retry', interval: 0"
1038
+ // and the server will hold the request, so when a response returns
1039
+ // we immediately call the server again (long polling)
1040
+ // Listeners can call disconnect(), so check the state after they run
1041
+ var action = _isDisconnected() ? 'none' : _advice.reconnect;
1042
+ switch (action)
1043
+ {
1044
+ case 'retry':
1045
+ _resetBackoff();
1046
+ _delayedConnect();
1047
+ break;
1048
+ case 'none':
1049
+ // Wait for the /meta/disconnect to arrive.
1050
+ _disconnect(false);
1051
+ break;
1052
+ default:
1053
+ throw 'Unrecognized advice action ' + action;
1054
+ }
1055
+ }
1056
+ else
1057
+ {
1058
+ _failConnect(message);
1059
+ }
1060
+ }
1061
+
1062
+ function _connectFailure(message)
1063
+ {
1064
+ _connected = false;
1065
+ _failConnect(message);
1066
+ }
1067
+
1068
+ function _failDisconnect(message)
1069
+ {
1070
+ _disconnect(true);
1071
+ _handleCallback(message);
1072
+ _notifyListeners('/meta/disconnect', message);
1073
+ _notifyListeners('/meta/unsuccessful', message);
1074
+ }
1075
+
1076
+ function _disconnectResponse(message)
1077
+ {
1078
+ if (message.successful)
1079
+ {
1080
+ // Wait for the /meta/connect to arrive.
1081
+ _disconnect(false);
1082
+ _handleCallback(message);
1083
+ _notifyListeners('/meta/disconnect', message);
1084
+ }
1085
+ else
1086
+ {
1087
+ _failDisconnect(message);
1088
+ }
1089
+ }
1090
+
1091
+ function _disconnectFailure(message)
1092
+ {
1093
+ _failDisconnect(message);
1094
+ }
1095
+
1096
+ function _failSubscribe(message)
1097
+ {
1098
+ var subscriptions = _listeners[message.subscription];
1099
+ if (subscriptions)
1100
+ {
1101
+ for (var i = subscriptions.length - 1; i >= 0; --i)
1102
+ {
1103
+ var subscription = subscriptions[i];
1104
+ if (subscription && !subscription.listener)
1105
+ {
1106
+ delete subscriptions[i];
1107
+ _cometd._debug('Removed failed subscription', subscription);
1108
+ break;
1109
+ }
1110
+ }
1111
+ }
1112
+ _handleCallback(message);
1113
+ _notifyListeners('/meta/subscribe', message);
1114
+ _notifyListeners('/meta/unsuccessful', message);
1115
+ }
1116
+
1117
+ function _subscribeResponse(message)
1118
+ {
1119
+ if (message.successful)
1120
+ {
1121
+ _handleCallback(message);
1122
+ _notifyListeners('/meta/subscribe', message);
1123
+ }
1124
+ else
1125
+ {
1126
+ _failSubscribe(message);
1127
+ }
1128
+ }
1129
+
1130
+ function _subscribeFailure(message)
1131
+ {
1132
+ _failSubscribe(message);
1133
+ }
1134
+
1135
+ function _failUnsubscribe(message)
1136
+ {
1137
+ _handleCallback(message);
1138
+ _notifyListeners('/meta/unsubscribe', message);
1139
+ _notifyListeners('/meta/unsuccessful', message);
1140
+ }
1141
+
1142
+ function _unsubscribeResponse(message)
1143
+ {
1144
+ if (message.successful)
1145
+ {
1146
+ _handleCallback(message);
1147
+ _notifyListeners('/meta/unsubscribe', message);
1148
+ }
1149
+ else
1150
+ {
1151
+ _failUnsubscribe(message);
1152
+ }
1153
+ }
1154
+
1155
+ function _unsubscribeFailure(message)
1156
+ {
1157
+ _failUnsubscribe(message);
1158
+ }
1159
+
1160
+ function _failMessage(message)
1161
+ {
1162
+ if (!_handleRemoteCall(message))
1163
+ {
1164
+ _handleCallback(message);
1165
+ _notifyListeners('/meta/publish', message);
1166
+ _notifyListeners('/meta/unsuccessful', message);
1167
+ }
1168
+ }
1169
+
1170
+ function _messageResponse(message)
1171
+ {
1172
+ if (message.data !== undefined)
1173
+ {
1174
+ if (!_handleRemoteCall(message))
1175
+ {
1176
+ _notifyListeners(message.channel, message);
1177
+ }
1178
+ }
1179
+ else
1180
+ {
1181
+ if (message.successful === undefined)
1182
+ {
1183
+ _cometd._warn('Unknown Bayeux Message', message);
1184
+ }
1185
+ else
1186
+ {
1187
+ if (message.successful)
1188
+ {
1189
+ _handleCallback(message);
1190
+ _notifyListeners('/meta/publish', message);
1191
+ }
1192
+ else
1193
+ {
1194
+ _failMessage(message);
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ function _messageFailure(failure)
1201
+ {
1202
+ _failMessage(failure);
1203
+ }
1204
+
1205
+ function _receive(message)
1206
+ {
1207
+ message = _applyIncomingExtensions(message);
1208
+ if (message === undefined || message === null)
1209
+ {
1210
+ return;
1211
+ }
1212
+
1213
+ _updateAdvice(message.advice);
1214
+
1215
+ var channel = message.channel;
1216
+ switch (channel)
1217
+ {
1218
+ case '/meta/handshake':
1219
+ _handshakeResponse(message);
1220
+ break;
1221
+ case '/meta/connect':
1222
+ _connectResponse(message);
1223
+ break;
1224
+ case '/meta/disconnect':
1225
+ _disconnectResponse(message);
1226
+ break;
1227
+ case '/meta/subscribe':
1228
+ _subscribeResponse(message);
1229
+ break;
1230
+ case '/meta/unsubscribe':
1231
+ _unsubscribeResponse(message);
1232
+ break;
1233
+ default:
1234
+ _messageResponse(message);
1235
+ break;
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ * Receives a message.
1241
+ * This method is exposed as a public so that extensions may inject
1242
+ * messages simulating that they had been received.
1243
+ */
1244
+ this.receive = _receive;
1245
+
1246
+ _handleMessages = function(rcvdMessages)
1247
+ {
1248
+ _cometd._debug('Received', rcvdMessages);
1249
+
1250
+ for (var i = 0; i < rcvdMessages.length; ++i)
1251
+ {
1252
+ var message = rcvdMessages[i];
1253
+ _receive(message);
1254
+ }
1255
+ };
1256
+
1257
+ _handleFailure = function(conduit, messages, failure)
1258
+ {
1259
+ _cometd._debug('handleFailure', conduit, messages, failure);
1260
+
1261
+ failure.transport = conduit;
1262
+ for (var i = 0; i < messages.length; ++i)
1263
+ {
1264
+ var message = messages[i];
1265
+ var failureMessage = {
1266
+ id: message.id,
1267
+ successful: false,
1268
+ channel: message.channel,
1269
+ failure: failure
1270
+ };
1271
+ failure.message = message;
1272
+ switch (message.channel)
1273
+ {
1274
+ case '/meta/handshake':
1275
+ _handshakeFailure(failureMessage);
1276
+ break;
1277
+ case '/meta/connect':
1278
+ _connectFailure(failureMessage);
1279
+ break;
1280
+ case '/meta/disconnect':
1281
+ _disconnectFailure(failureMessage);
1282
+ break;
1283
+ case '/meta/subscribe':
1284
+ failureMessage.subscription = message.subscription;
1285
+ _subscribeFailure(failureMessage);
1286
+ break;
1287
+ case '/meta/unsubscribe':
1288
+ failureMessage.subscription = message.subscription;
1289
+ _unsubscribeFailure(failureMessage);
1290
+ break;
1291
+ default:
1292
+ _messageFailure(failureMessage);
1293
+ break;
1294
+ }
1295
+ }
1296
+ };
1297
+
1298
+ function _hasSubscriptions(channel)
1299
+ {
1300
+ var subscriptions = _listeners[channel];
1301
+ if (subscriptions)
1302
+ {
1303
+ for (var i = 0; i < subscriptions.length; ++i)
1304
+ {
1305
+ if (subscriptions[i])
1306
+ {
1307
+ return true;
1308
+ }
1309
+ }
1310
+ }
1311
+ return false;
1312
+ }
1313
+
1314
+ function _resolveScopedCallback(scope, callback)
1315
+ {
1316
+ var delegate = {
1317
+ scope: scope,
1318
+ method: callback
1319
+ };
1320
+ if (_isFunction(scope))
1321
+ {
1322
+ delegate.scope = undefined;
1323
+ delegate.method = scope;
1324
+ }
1325
+ else
1326
+ {
1327
+ if (_isString(callback))
1328
+ {
1329
+ if (!scope)
1330
+ {
1331
+ throw 'Invalid scope ' + scope;
1332
+ }
1333
+ delegate.method = scope[callback];
1334
+ if (!_isFunction(delegate.method))
1335
+ {
1336
+ throw 'Invalid callback ' + callback + ' for scope ' + scope;
1337
+ }
1338
+ }
1339
+ else if (!_isFunction(callback))
1340
+ {
1341
+ throw 'Invalid callback ' + callback;
1342
+ }
1343
+ }
1344
+ return delegate;
1345
+ }
1346
+
1347
+ function _addListener(channel, scope, callback, isListener)
1348
+ {
1349
+ // The data structure is a map<channel, subscription[]>, where each subscription
1350
+ // holds the callback to be called and its scope.
1351
+
1352
+ var delegate = _resolveScopedCallback(scope, callback);
1353
+ _cometd._debug('Adding', isListener ? 'listener' : 'subscription', 'on', channel, 'with scope', delegate.scope, 'and callback', delegate.method);
1354
+
1355
+ var subscription = {
1356
+ channel: channel,
1357
+ scope: delegate.scope,
1358
+ callback: delegate.method,
1359
+ listener: isListener
1360
+ };
1361
+
1362
+ var subscriptions = _listeners[channel];
1363
+ if (!subscriptions)
1364
+ {
1365
+ subscriptions = [];
1366
+ _listeners[channel] = subscriptions;
1367
+ }
1368
+
1369
+ // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
1370
+ // Note that if:
1371
+ // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
1372
+ // then:
1373
+ // hc==3, a.join()=='a',,'c', a.length==3
1374
+ subscription.id = subscriptions.push(subscription) - 1;
1375
+
1376
+ _cometd._debug('Added', isListener ? 'listener' : 'subscription', subscription);
1377
+
1378
+ // For backward compatibility: we used to return [channel, subscription.id]
1379
+ subscription[0] = channel;
1380
+ subscription[1] = subscription.id;
1381
+
1382
+ return subscription;
1383
+ }
1384
+
1385
+ //
1386
+ // PUBLIC API
1387
+ //
1388
+
1389
+ /**
1390
+ * Registers the given transport under the given transport type.
1391
+ * The optional index parameter specifies the "priority" at which the
1392
+ * transport is registered (where 0 is the max priority).
1393
+ * If a transport with the same type is already registered, this function
1394
+ * does nothing and returns false.
1395
+ * @param type the transport type
1396
+ * @param transport the transport object
1397
+ * @param index the index at which this transport is to be registered
1398
+ * @return true if the transport has been registered, false otherwise
1399
+ * @see #unregisterTransport(type)
1400
+ */
1401
+ this.registerTransport = function(type, transport, index)
1402
+ {
1403
+ var result = _transports.add(type, transport, index);
1404
+ if (result)
1405
+ {
1406
+ this._debug('Registered transport', type);
1407
+
1408
+ if (_isFunction(transport.registered))
1409
+ {
1410
+ transport.registered(type, this);
1411
+ }
1412
+ }
1413
+ return result;
1414
+ };
1415
+
1416
+ /**
1417
+ * @return an array of all registered transport types
1418
+ */
1419
+ this.getTransportTypes = function()
1420
+ {
1421
+ return _transports.getTransportTypes();
1422
+ };
1423
+
1424
+ /**
1425
+ * Unregisters the transport with the given transport type.
1426
+ * @param type the transport type to unregister
1427
+ * @return the transport that has been unregistered,
1428
+ * or null if no transport was previously registered under the given transport type
1429
+ */
1430
+ this.unregisterTransport = function(type)
1431
+ {
1432
+ var transport = _transports.remove(type);
1433
+ if (transport !== null)
1434
+ {
1435
+ this._debug('Unregistered transport', type);
1436
+
1437
+ if (_isFunction(transport.unregistered))
1438
+ {
1439
+ transport.unregistered();
1440
+ }
1441
+ }
1442
+ return transport;
1443
+ };
1444
+
1445
+ this.unregisterTransports = function()
1446
+ {
1447
+ _transports.clear();
1448
+ };
1449
+
1450
+ this.findTransport = function(name)
1451
+ {
1452
+ return _transports.find(name);
1453
+ };
1454
+
1455
+ /**
1456
+ * Configures the initial Bayeux communication with the Bayeux server.
1457
+ * Configuration is passed via an object that must contain a mandatory field <code>url</code>
1458
+ * of type string containing the URL of the Bayeux server.
1459
+ * @param configuration the configuration object
1460
+ */
1461
+ this.configure = function(configuration)
1462
+ {
1463
+ _configure.call(this, configuration);
1464
+ };
1465
+
1466
+ /**
1467
+ * Configures and establishes the Bayeux communication with the Bayeux server
1468
+ * via a handshake and a subsequent connect.
1469
+ * @param configuration the configuration object
1470
+ * @param handshakeProps an object to be merged with the handshake message
1471
+ * @see #configure(configuration)
1472
+ * @see #handshake(handshakeProps)
1473
+ */
1474
+ this.init = function(configuration, handshakeProps)
1475
+ {
1476
+ this.configure(configuration);
1477
+ this.handshake(handshakeProps);
1478
+ };
1479
+
1480
+ /**
1481
+ * Establishes the Bayeux communication with the Bayeux server
1482
+ * via a handshake and a subsequent connect.
1483
+ * @param handshakeProps an object to be merged with the handshake message
1484
+ * @param handshakeCallback a function to be invoked when the handshake is acknowledged
1485
+ */
1486
+ this.handshake = function(handshakeProps, handshakeCallback)
1487
+ {
1488
+ _setStatus('disconnected');
1489
+ _reestablish = false;
1490
+ _handshake(handshakeProps, handshakeCallback);
1491
+ };
1492
+
1493
+ /**
1494
+ * Disconnects from the Bayeux server.
1495
+ * It is possible to suggest to attempt a synchronous disconnect, but this feature
1496
+ * may only be available in certain transports (for example, long-polling may support
1497
+ * it, callback-polling certainly does not).
1498
+ * @param sync whether attempt to perform a synchronous disconnect
1499
+ * @param disconnectProps an object to be merged with the disconnect message
1500
+ * @param disconnectCallback a function to be invoked when the disconnect is acknowledged
1501
+ */
1502
+ this.disconnect = function(sync, disconnectProps, disconnectCallback)
1503
+ {
1504
+ if (_isDisconnected())
1505
+ {
1506
+ return;
1507
+ }
1508
+
1509
+ if (typeof sync !== 'boolean')
1510
+ {
1511
+ disconnectCallback = disconnectProps;
1512
+ disconnectProps = sync;
1513
+ sync = false;
1514
+ }
1515
+ if (_isFunction(disconnectProps))
1516
+ {
1517
+ disconnectCallback = disconnectProps;
1518
+ disconnectProps = undefined;
1519
+ }
1520
+
1521
+ var bayeuxMessage = {
1522
+ id: _nextMessageId(),
1523
+ channel: '/meta/disconnect',
1524
+ _callback: disconnectCallback
1525
+ };
1526
+ // Do not allow the user to override important fields.
1527
+ var message = this._mixin(false, {}, disconnectProps, bayeuxMessage);
1528
+ _setStatus('disconnecting');
1529
+ _send(sync === true, [message], false, 'disconnect');
1530
+ };
1531
+
1532
+ /**
1533
+ * Marks the start of a batch of application messages to be sent to the server
1534
+ * in a single request, obtaining a single response containing (possibly) many
1535
+ * application reply messages.
1536
+ * Messages are held in a queue and not sent until {@link #endBatch()} is called.
1537
+ * If startBatch() is called multiple times, then an equal number of endBatch()
1538
+ * calls must be made to close and send the batch of messages.
1539
+ * @see #endBatch()
1540
+ */
1541
+ this.startBatch = function()
1542
+ {
1543
+ _startBatch();
1544
+ };
1545
+
1546
+ /**
1547
+ * Marks the end of a batch of application messages to be sent to the server
1548
+ * in a single request.
1549
+ * @see #startBatch()
1550
+ */
1551
+ this.endBatch = function()
1552
+ {
1553
+ _endBatch();
1554
+ };
1555
+
1556
+ /**
1557
+ * Executes the given callback in the given scope, surrounded by a {@link #startBatch()}
1558
+ * and {@link #endBatch()} calls.
1559
+ * @param scope the scope of the callback, may be omitted
1560
+ * @param callback the callback to be executed within {@link #startBatch()} and {@link #endBatch()} calls
1561
+ */
1562
+ this.batch = function(scope, callback)
1563
+ {
1564
+ var delegate = _resolveScopedCallback(scope, callback);
1565
+ this.startBatch();
1566
+ try
1567
+ {
1568
+ delegate.method.call(delegate.scope);
1569
+ this.endBatch();
1570
+ }
1571
+ catch (x)
1572
+ {
1573
+ this._info('Exception during execution of batch', x);
1574
+ this.endBatch();
1575
+ throw x;
1576
+ }
1577
+ };
1578
+
1579
+ /**
1580
+ * Adds a listener for bayeux messages, performing the given callback in the given scope
1581
+ * when a message for the given channel arrives.
1582
+ * @param channel the channel the listener is interested to
1583
+ * @param scope the scope of the callback, may be omitted
1584
+ * @param callback the callback to call when a message is sent to the channel
1585
+ * @returns the subscription handle to be passed to {@link #removeListener(object)}
1586
+ * @see #removeListener(subscription)
1587
+ */
1588
+ this.addListener = function(channel, scope, callback)
1589
+ {
1590
+ if (arguments.length < 2)
1591
+ {
1592
+ throw 'Illegal arguments number: required 2, got ' + arguments.length;
1593
+ }
1594
+ if (!_isString(channel))
1595
+ {
1596
+ throw 'Illegal argument type: channel must be a string';
1597
+ }
1598
+
1599
+ return _addListener(channel, scope, callback, true);
1600
+ };
1601
+
1602
+ /**
1603
+ * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
1604
+ * @param subscription the subscription to unsubscribe.
1605
+ * @see #addListener(channel, scope, callback)
1606
+ */
1607
+ this.removeListener = function(subscription)
1608
+ {
1609
+ // Beware of subscription.id == 0, which is falsy => cannot use !subscription.id
1610
+ if (!subscription || !subscription.channel || !("id" in subscription))
1611
+ {
1612
+ throw 'Invalid argument: expected subscription, not ' + subscription;
1613
+ }
1614
+
1615
+ _removeListener(subscription);
1616
+ };
1617
+
1618
+ /**
1619
+ * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
1620
+ * {@link #subscribe(channel, scope, callback)}.
1621
+ */
1622
+ this.clearListeners = function()
1623
+ {
1624
+ _listeners = {};
1625
+ };
1626
+
1627
+ /**
1628
+ * Subscribes to the given channel, performing the given callback in the given scope
1629
+ * when a message for the channel arrives.
1630
+ * @param channel the channel to subscribe to
1631
+ * @param scope the scope of the callback, may be omitted
1632
+ * @param callback the callback to call when a message is sent to the channel
1633
+ * @param subscribeProps an object to be merged with the subscribe message
1634
+ * @param subscribeCallback a function to be invoked when the subscription is acknowledged
1635
+ * @return the subscription handle to be passed to {@link #unsubscribe(object)}
1636
+ */
1637
+ this.subscribe = function(channel, scope, callback, subscribeProps, subscribeCallback)
1638
+ {
1639
+ if (arguments.length < 2)
1640
+ {
1641
+ throw 'Illegal arguments number: required 2, got ' + arguments.length;
1642
+ }
1643
+ if (!_isString(channel))
1644
+ {
1645
+ throw 'Illegal argument type: channel must be a string';
1646
+ }
1647
+ if (_isDisconnected())
1648
+ {
1649
+ throw 'Illegal state: already disconnected';
1650
+ }
1651
+
1652
+ // Normalize arguments
1653
+ if (_isFunction(scope))
1654
+ {
1655
+ subscribeCallback = subscribeProps;
1656
+ subscribeProps = callback;
1657
+ callback = scope;
1658
+ scope = undefined;
1659
+ }
1660
+ if (_isFunction(subscribeProps))
1661
+ {
1662
+ subscribeCallback = subscribeProps;
1663
+ subscribeProps = undefined;
1664
+ }
1665
+
1666
+ // Only send the message to the server if this client has not yet subscribed to the channel
1667
+ var send = !_hasSubscriptions(channel);
1668
+
1669
+ var subscription = _addListener(channel, scope, callback, false);
1670
+
1671
+ if (send)
1672
+ {
1673
+ // Send the subscription message after the subscription registration to avoid
1674
+ // races where the server would send a message to the subscribers, but here
1675
+ // on the client the subscription has not been added yet to the data structures
1676
+ var bayeuxMessage = {
1677
+ id: _nextMessageId(),
1678
+ channel: '/meta/subscribe',
1679
+ subscription: channel,
1680
+ _callback: subscribeCallback
1681
+ };
1682
+ // Do not allow the user to override important fields.
1683
+ var message = this._mixin(false, {}, subscribeProps, bayeuxMessage);
1684
+ _queueSend(message);
1685
+ }
1686
+
1687
+ return subscription;
1688
+ };
1689
+
1690
+ /**
1691
+ * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
1692
+ * @param subscription the subscription to unsubscribe.
1693
+ * @param unsubscribeProps an object to be merged with the unsubscribe message
1694
+ * @param unsubscribeCallback a function to be invoked when the unsubscription is acknowledged
1695
+ */
1696
+ this.unsubscribe = function(subscription, unsubscribeProps, unsubscribeCallback)
1697
+ {
1698
+ if (arguments.length < 1)
1699
+ {
1700
+ throw 'Illegal arguments number: required 1, got ' + arguments.length;
1701
+ }
1702
+ if (_isDisconnected())
1703
+ {
1704
+ throw 'Illegal state: already disconnected';
1705
+ }
1706
+
1707
+ if (_isFunction(unsubscribeProps))
1708
+ {
1709
+ unsubscribeCallback = unsubscribeProps;
1710
+ unsubscribeProps = undefined;
1711
+ }
1712
+
1713
+ // Remove the local listener before sending the message
1714
+ // This ensures that if the server fails, this client does not get notifications
1715
+ this.removeListener(subscription);
1716
+
1717
+ var channel = subscription.channel;
1718
+ // Only send the message to the server if this client unsubscribes the last subscription
1719
+ if (!_hasSubscriptions(channel))
1720
+ {
1721
+ var bayeuxMessage = {
1722
+ id: _nextMessageId(),
1723
+ channel: '/meta/unsubscribe',
1724
+ subscription: channel,
1725
+ _callback: unsubscribeCallback
1726
+ };
1727
+ // Do not allow the user to override important fields.
1728
+ var message = this._mixin(false, {}, unsubscribeProps, bayeuxMessage);
1729
+ _queueSend(message);
1730
+ }
1731
+ };
1732
+
1733
+ this.resubscribe = function(subscription, subscribeProps)
1734
+ {
1735
+ _removeSubscription(subscription);
1736
+ if (subscription)
1737
+ {
1738
+ return this.subscribe(subscription.channel, subscription.scope, subscription.callback, subscribeProps);
1739
+ }
1740
+ return undefined;
1741
+ };
1742
+
1743
+ /**
1744
+ * Removes all subscriptions added via {@link #subscribe(channel, scope, callback, subscribeProps)},
1745
+ * but does not remove the listeners added via {@link addListener(channel, scope, callback)}.
1746
+ */
1747
+ this.clearSubscriptions = function()
1748
+ {
1749
+ _clearSubscriptions();
1750
+ };
1751
+
1752
+ /**
1753
+ * Publishes a message on the given channel, containing the given content.
1754
+ * @param channel the channel to publish the message to
1755
+ * @param content the content of the message
1756
+ * @param publishProps an object to be merged with the publish message
1757
+ * @param publishCallback a function to be invoked when the publish is acknowledged by the server
1758
+ */
1759
+ this.publish = function(channel, content, publishProps, publishCallback)
1760
+ {
1761
+ if (arguments.length < 1)
1762
+ {
1763
+ throw 'Illegal arguments number: required 1, got ' + arguments.length;
1764
+ }
1765
+ if (!_isString(channel))
1766
+ {
1767
+ throw 'Illegal argument type: channel must be a string';
1768
+ }
1769
+ if (/^\/meta\//.test(channel))
1770
+ {
1771
+ throw 'Illegal argument: cannot publish to meta channels';
1772
+ }
1773
+ if (_isDisconnected())
1774
+ {
1775
+ throw 'Illegal state: already disconnected';
1776
+ }
1777
+
1778
+ if (_isFunction(content))
1779
+ {
1780
+ publishCallback = content;
1781
+ content = publishProps = {};
1782
+ }
1783
+ else if (_isFunction(publishProps))
1784
+ {
1785
+ publishCallback = publishProps;
1786
+ publishProps = {};
1787
+ }
1788
+
1789
+ var bayeuxMessage = {
1790
+ id: _nextMessageId(),
1791
+ channel: channel,
1792
+ data: content,
1793
+ _callback: publishCallback
1794
+ };
1795
+ // Do not allow the user to override important fields.
1796
+ var message = this._mixin(false, {}, publishProps, bayeuxMessage);
1797
+ _queueSend(message);
1798
+ };
1799
+
1800
+ this.remoteCall = function(target, content, timeout, callback)
1801
+ {
1802
+ if (arguments.length < 1)
1803
+ {
1804
+ throw 'Illegal arguments number: required 1, got ' + arguments.length;
1805
+ }
1806
+ if (!_isString(target))
1807
+ {
1808
+ throw 'Illegal argument type: target must be a string';
1809
+ }
1810
+ if (_isDisconnected())
1811
+ {
1812
+ throw 'Illegal state: already disconnected';
1813
+ }
1814
+
1815
+ if (_isFunction(content))
1816
+ {
1817
+ callback = content;
1818
+ content = {};
1819
+ timeout = _config.maxNetworkDelay;
1820
+ }
1821
+ else if (_isFunction(timeout))
1822
+ {
1823
+ callback = timeout;
1824
+ timeout = _config.maxNetworkDelay;
1825
+ }
1826
+
1827
+ if (typeof timeout !== 'number')
1828
+ {
1829
+ throw 'Illegal argument type: timeout must be a number';
1830
+ }
1831
+
1832
+ if (!target.match(/^\//))
1833
+ {
1834
+ target = '/' + target;
1835
+ }
1836
+ var channel = '/service' + target;
1837
+
1838
+ var bayeuxMessage = {
1839
+ id: _nextMessageId(),
1840
+ channel: channel,
1841
+ data: content
1842
+ };
1843
+
1844
+ var context = {
1845
+ callback: callback
1846
+ };
1847
+ if (timeout > 0)
1848
+ {
1849
+ context.timeout = org.cometd.Utils.setTimeout(_cometd, function()
1850
+ {
1851
+ _cometd._debug('Timing out remote call', bayeuxMessage, 'after', timeout, 'ms');
1852
+ _failMessage({
1853
+ id: bayeuxMessage.id,
1854
+ error: '406::timeout',
1855
+ successful: false,
1856
+ failure: {
1857
+ message : bayeuxMessage,
1858
+ reason: 'Remote Call Timeout'
1859
+ }
1860
+ });
1861
+ }, timeout);
1862
+ _cometd._debug('Scheduled remote call timeout', bayeuxMessage, 'in', timeout, 'ms');
1863
+ }
1864
+ _remoteCalls[bayeuxMessage.id] = context;
1865
+
1866
+ _queueSend(bayeuxMessage);
1867
+ };
1868
+
1869
+ /**
1870
+ * Returns a string representing the status of the bayeux communication with the Bayeux server.
1871
+ */
1872
+ this.getStatus = function()
1873
+ {
1874
+ return _status;
1875
+ };
1876
+
1877
+ /**
1878
+ * Returns whether this instance has been disconnected.
1879
+ */
1880
+ this.isDisconnected = _isDisconnected;
1881
+
1882
+ /**
1883
+ * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
1884
+ * Default value is 1 second, which means if there is a persistent failure the retries will happen
1885
+ * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
1886
+ * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
1887
+ * @param period the backoff period to set
1888
+ * @see #getBackoffIncrement()
1889
+ */
1890
+ this.setBackoffIncrement = function(period)
1891
+ {
1892
+ _config.backoffIncrement = period;
1893
+ };
1894
+
1895
+ /**
1896
+ * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
1897
+ * @see #setBackoffIncrement(period)
1898
+ */
1899
+ this.getBackoffIncrement = function()
1900
+ {
1901
+ return _config.backoffIncrement;
1902
+ };
1903
+
1904
+ /**
1905
+ * Returns the backoff period to wait before retrying an unsuccessful or failed message.
1906
+ */
1907
+ this.getBackoffPeriod = function()
1908
+ {
1909
+ return _backoff;
1910
+ };
1911
+
1912
+ /**
1913
+ * Sets the log level for console logging.
1914
+ * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
1915
+ * less verbose to more verbose.
1916
+ * @param level the log level string
1917
+ */
1918
+ this.setLogLevel = function(level)
1919
+ {
1920
+ _config.logLevel = level;
1921
+ };
1922
+
1923
+ /**
1924
+ * Registers an extension whose callbacks are called for every incoming message
1925
+ * (that comes from the server to this client implementation) and for every
1926
+ * outgoing message (that originates from this client implementation for the
1927
+ * server).
1928
+ * The format of the extension object is the following:
1929
+ * <pre>
1930
+ * {
1931
+ * incoming: function(message) { ... },
1932
+ * outgoing: function(message) { ... }
1933
+ * }
1934
+ * </pre>
1935
+ * Both properties are optional, but if they are present they will be called
1936
+ * respectively for each incoming message and for each outgoing message.
1937
+ * @param name the name of the extension
1938
+ * @param extension the extension to register
1939
+ * @return true if the extension was registered, false otherwise
1940
+ * @see #unregisterExtension(name)
1941
+ */
1942
+ this.registerExtension = function(name, extension)
1943
+ {
1944
+ if (arguments.length < 2)
1945
+ {
1946
+ throw 'Illegal arguments number: required 2, got ' + arguments.length;
1947
+ }
1948
+ if (!_isString(name))
1949
+ {
1950
+ throw 'Illegal argument type: extension name must be a string';
1951
+ }
1952
+
1953
+ var existing = false;
1954
+ for (var i = 0; i < _extensions.length; ++i)
1955
+ {
1956
+ var existingExtension = _extensions[i];
1957
+ if (existingExtension.name === name)
1958
+ {
1959
+ existing = true;
1960
+ break;
1961
+ }
1962
+ }
1963
+ if (!existing)
1964
+ {
1965
+ _extensions.push({
1966
+ name: name,
1967
+ extension: extension
1968
+ });
1969
+ this._debug('Registered extension', name);
1970
+
1971
+ // Callback for extensions
1972
+ if (_isFunction(extension.registered))
1973
+ {
1974
+ extension.registered(name, this);
1975
+ }
1976
+
1977
+ return true;
1978
+ }
1979
+ else
1980
+ {
1981
+ this._info('Could not register extension with name', name, 'since another extension with the same name already exists');
1982
+ return false;
1983
+ }
1984
+ };
1985
+
1986
+ /**
1987
+ * Unregister an extension previously registered with
1988
+ * {@link #registerExtension(name, extension)}.
1989
+ * @param name the name of the extension to unregister.
1990
+ * @return true if the extension was unregistered, false otherwise
1991
+ */
1992
+ this.unregisterExtension = function(name)
1993
+ {
1994
+ if (!_isString(name))
1995
+ {
1996
+ throw 'Illegal argument type: extension name must be a string';
1997
+ }
1998
+
1999
+ var unregistered = false;
2000
+ for (var i = 0; i < _extensions.length; ++i)
2001
+ {
2002
+ var extension = _extensions[i];
2003
+ if (extension.name === name)
2004
+ {
2005
+ _extensions.splice(i, 1);
2006
+ unregistered = true;
2007
+ this._debug('Unregistered extension', name);
2008
+
2009
+ // Callback for extensions
2010
+ var ext = extension.extension;
2011
+ if (_isFunction(ext.unregistered))
2012
+ {
2013
+ ext.unregistered();
2014
+ }
2015
+
2016
+ break;
2017
+ }
2018
+ }
2019
+ return unregistered;
2020
+ };
2021
+
2022
+ /**
2023
+ * Find the extension registered with the given name.
2024
+ * @param name the name of the extension to find
2025
+ * @return the extension found or null if no extension with the given name has been registered
2026
+ */
2027
+ this.getExtension = function(name)
2028
+ {
2029
+ for (var i = 0; i < _extensions.length; ++i)
2030
+ {
2031
+ var extension = _extensions[i];
2032
+ if (extension.name === name)
2033
+ {
2034
+ return extension.extension;
2035
+ }
2036
+ }
2037
+ return null;
2038
+ };
2039
+
2040
+ /**
2041
+ * Returns the name assigned to this CometD object, or the string 'default'
2042
+ * if no name has been explicitly passed as parameter to the constructor.
2043
+ */
2044
+ this.getName = function()
2045
+ {
2046
+ return _name;
2047
+ };
2048
+
2049
+ /**
2050
+ * Returns the clientId assigned by the Bayeux server during handshake.
2051
+ */
2052
+ this.getClientId = function()
2053
+ {
2054
+ return _clientId;
2055
+ };
2056
+
2057
+ /**
2058
+ * Returns the URL of the Bayeux server.
2059
+ */
2060
+ this.getURL = function()
2061
+ {
2062
+ if (_transport && typeof _config.urls === 'object')
2063
+ {
2064
+ var url = _config.urls[_transport.getType()];
2065
+ if (url)
2066
+ {
2067
+ return url;
2068
+ }
2069
+ }
2070
+ return _config.url;
2071
+ };
2072
+
2073
+ this.getTransport = function()
2074
+ {
2075
+ return _transport;
2076
+ };
2077
+
2078
+ this.getConfiguration = function()
2079
+ {
2080
+ return this._mixin(true, {}, _config);
2081
+ };
2082
+
2083
+ this.getAdvice = function()
2084
+ {
2085
+ return this._mixin(true, {}, _advice);
2086
+ };
2087
+
2088
+ // Use an alias to be less dependent on browser's quirks.
2089
+ org.cometd.WebSocket = window.WebSocket;
2090
+ };