blob: a91435956c1adcf979ac4b4b21bca44999fda6c5 [file] [log] [blame]
Xander Uiterlinden6d5c1062012-06-06 07:03:59 +00001/*
2 * Copyright (c) 2010 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17if (typeof dojo !== 'undefined')
18{
19 dojo.provide('org.cometd');
20}
21else
22{
23 // Namespaces for the cometd implementation
24 this.org = this.org || {};
25 org.cometd = {};
26}
27
28org.cometd.JSON = {};
29org.cometd.JSON.toJSON = org.cometd.JSON.fromJSON = function(object)
30{
31 throw 'Abstract';
32};
33
34org.cometd.Utils = {};
35
36org.cometd.Utils.isString = function(value)
37{
38 if (value === undefined || value === null)
39 {
40 return false;
41 }
42 return typeof value === 'string' || value instanceof String;
43};
44
45org.cometd.Utils.isArray = function(value)
46{
47 if (value === undefined || value === null)
48 {
49 return false;
50 }
51 return value instanceof Array;
52};
53
54/**
55 * Returns whether the given element is contained into the given array.
56 * @param element the element to check presence for
57 * @param array the array to check for the element presence
58 * @return the index of the element, if present, or a negative index if the element is not present
59 */
60org.cometd.Utils.inArray = function(element, array)
61{
62 for (var i = 0; i < array.length; ++i)
63 {
64 if (element === array[i])
65 {
66 return i;
67 }
68 }
69 return -1;
70};
71
72org.cometd.Utils.setTimeout = function(cometd, funktion, delay)
73{
74 return window.setTimeout(function()
75 {
76 try
77 {
78 funktion();
79 }
80 catch (x)
81 {
82 cometd._debug('Exception invoking timed function', funktion, x);
83 }
84 }, delay);
85};
86
87org.cometd.Utils.clearTimeout = function(timeoutHandle)
88{
89 window.clearTimeout(timeoutHandle);
90};
91
92/**
93 * A registry for transports used by the Cometd object.
94 */
95org.cometd.TransportRegistry = function()
96{
97 var _types = [];
98 var _transports = {};
99
100 this.getTransportTypes = function()
101 {
102 return _types.slice(0);
103 };
104
105 this.findTransportTypes = function(version, crossDomain, url)
106 {
107 var result = [];
108 for (var i = 0; i < _types.length; ++i)
109 {
110 var type = _types[i];
111 if (_transports[type].accept(version, crossDomain, url) === true)
112 {
113 result.push(type);
114 }
115 }
116 return result;
117 };
118
119 this.negotiateTransport = function(types, version, crossDomain, url)
120 {
121 for (var i = 0; i < _types.length; ++i)
122 {
123 var type = _types[i];
124 for (var j = 0; j < types.length; ++j)
125 {
126 if (type === types[j])
127 {
128 var transport = _transports[type];
129 if (transport.accept(version, crossDomain, url) === true)
130 {
131 return transport;
132 }
133 }
134 }
135 }
136 return null;
137 };
138
139 this.add = function(type, transport, index)
140 {
141 var existing = false;
142 for (var i = 0; i < _types.length; ++i)
143 {
144 if (_types[i] === type)
145 {
146 existing = true;
147 break;
148 }
149 }
150
151 if (!existing)
152 {
153 if (typeof index !== 'number')
154 {
155 _types.push(type);
156 }
157 else
158 {
159 _types.splice(index, 0, type);
160 }
161 _transports[type] = transport;
162 }
163
164 return !existing;
165 };
166
167 this.find = function(type)
168 {
169 for (var i = 0; i < _types.length; ++i)
170 {
171 if (_types[i] === type)
172 {
173 return _transports[type];
174 }
175 }
176 return null;
177 };
178
179 this.remove = function(type)
180 {
181 for (var i = 0; i < _types.length; ++i)
182 {
183 if (_types[i] === type)
184 {
185 _types.splice(i, 1);
186 var transport = _transports[type];
187 delete _transports[type];
188 return transport;
189 }
190 }
191 return null;
192 };
193
194 this.clear = function()
195 {
196 _types = [];
197 _transports = {};
198 };
199
200 this.reset = function()
201 {
202 for (var i = 0; i < _types.length; ++i)
203 {
204 _transports[_types[i]].reset();
205 }
206 };
207};
208
209/**
210 * Base object with the common functionality for transports.
211 */
212org.cometd.Transport = function()
213{
214 var _type;
215 var _cometd;
216
217 /**
218 * Function invoked just after a transport has been successfully registered.
219 * @param type the type of transport (for example 'long-polling')
220 * @param cometd the cometd object this transport has been registered to
221 * @see #unregistered()
222 */
223 this.registered = function(type, cometd)
224 {
225 _type = type;
226 _cometd = cometd;
227 };
228
229 /**
230 * Function invoked just after a transport has been successfully unregistered.
231 * @see #registered(type, cometd)
232 */
233 this.unregistered = function()
234 {
235 _type = null;
236 _cometd = null;
237 };
238
239 this._debug = function()
240 {
241 _cometd._debug.apply(_cometd, arguments);
242 };
243
244 this._mixin = function()
245 {
246 return _cometd._mixin.apply(_cometd, arguments);
247 };
248
249 this.getConfiguration = function()
250 {
251 return _cometd.getConfiguration();
252 };
253
254 this.getAdvice = function()
255 {
256 return _cometd.getAdvice();
257 };
258
259 this.setTimeout = function(funktion, delay)
260 {
261 return org.cometd.Utils.setTimeout(_cometd, funktion, delay);
262 };
263
264 this.clearTimeout = function(handle)
265 {
266 org.cometd.Utils.clearTimeout(handle);
267 };
268
269 /**
270 * Converts the given response into an array of bayeux messages
271 * @param response the response to convert
272 * @return an array of bayeux messages obtained by converting the response
273 */
274 this.convertToMessages = function (response)
275 {
276 if (org.cometd.Utils.isString(response))
277 {
278 try
279 {
280 return org.cometd.JSON.fromJSON(response);
281 }
282 catch(x)
283 {
284 this._debug('Could not convert to JSON the following string', '"' + response + '"');
285 throw x;
286 }
287 }
288 if (org.cometd.Utils.isArray(response))
289 {
290 return response;
291 }
292 if (response === undefined || response === null)
293 {
294 return [];
295 }
296 if (response instanceof Object)
297 {
298 return [response];
299 }
300 throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
301 };
302
303 /**
304 * Returns whether this transport can work for the given version and cross domain communication case.
305 * @param version a string indicating the transport version
306 * @param crossDomain a boolean indicating whether the communication is cross domain
307 * @return true if this transport can work for the given version and cross domain communication case,
308 * false otherwise
309 */
310 this.accept = function(version, crossDomain, url)
311 {
312 throw 'Abstract';
313 };
314
315 /**
316 * Returns the type of this transport.
317 * @see #registered(type, cometd)
318 */
319 this.getType = function()
320 {
321 return _type;
322 };
323
324 this.send = function(envelope, metaConnect)
325 {
326 throw 'Abstract';
327 };
328
329 this.reset = function()
330 {
331 this._debug('Transport', _type, 'reset');
332 };
333
334 this.abort = function()
335 {
336 this._debug('Transport', _type, 'aborted');
337 };
338
339 this.toString = function()
340 {
341 return this.getType();
342 };
343};
344
345org.cometd.Transport.derive = function(baseObject)
346{
347 function F() {}
348 F.prototype = baseObject;
349 return new F();
350};
351
352/**
353 * Base object with the common functionality for transports based on requests.
354 * The key responsibility is to allow at most 2 outstanding requests to the server,
355 * to avoid that requests are sent behind a long poll.
356 * To achieve this, we have one reserved request for the long poll, and all other
357 * requests are serialized one after the other.
358 */
359org.cometd.RequestTransport = function()
360{
361 var _super = new org.cometd.Transport();
362 var _self = org.cometd.Transport.derive(_super);
363 var _requestIds = 0;
364 var _metaConnectRequest = null;
365 var _requests = [];
366 var _envelopes = [];
367
368 function _coalesceEnvelopes(envelope)
369 {
370 while (_envelopes.length > 0)
371 {
372 var envelopeAndRequest = _envelopes[0];
373 var newEnvelope = envelopeAndRequest[0];
374 var newRequest = envelopeAndRequest[1];
375 if (newEnvelope.url === envelope.url &&
376 newEnvelope.sync === envelope.sync)
377 {
378 _envelopes.shift();
379 envelope.messages = envelope.messages.concat(newEnvelope.messages);
380 this._debug('Coalesced', newEnvelope.messages.length, 'messages from request', newRequest.id);
381 continue;
382 }
383 break;
384 }
385 }
386
387 function _transportSend(envelope, request)
388 {
389 this.transportSend(envelope, request);
390 request.expired = false;
391
392 if (!envelope.sync)
393 {
394 var maxDelay = this.getConfiguration().maxNetworkDelay;
395 var delay = maxDelay;
396 if (request.metaConnect === true)
397 {
398 delay += this.getAdvice().timeout;
399 }
400
401 this._debug('Transport', this.getType(), 'waiting at most', delay, 'ms for the response, maxNetworkDelay', maxDelay);
402
403 var self = this;
404 request.timeout = this.setTimeout(function()
405 {
406 request.expired = true;
407 if (request.xhr)
408 {
409 request.xhr.abort();
410 }
411 var errorMessage = 'Request ' + request.id + ' of transport ' + self.getType() + ' exceeded ' + delay + ' ms max network delay';
412 self._debug(errorMessage);
413 self.complete(request, false, request.metaConnect);
414 envelope.onFailure(request.xhr, envelope.messages, 'timeout', errorMessage);
415 }, delay);
416 }
417 }
418
419 function _queueSend(envelope)
420 {
421 var requestId = ++_requestIds;
422 var request = {
423 id: requestId,
424 metaConnect: false
425 };
426
427 // Consider the metaConnect requests which should always be present
428 if (_requests.length < this.getConfiguration().maxConnections - 1)
429 {
430 _requests.push(request);
431 _transportSend.call(this, envelope, request);
432 }
433 else
434 {
435 this._debug('Transport', this.getType(), 'queueing request', requestId, 'envelope', envelope);
436 _envelopes.push([envelope, request]);
437 }
438 }
439
440 function _metaConnectComplete(request)
441 {
442 var requestId = request.id;
443 this._debug('Transport', this.getType(), 'metaConnect complete, request', requestId);
444 if (_metaConnectRequest !== null && _metaConnectRequest.id !== requestId)
445 {
446 throw 'Longpoll request mismatch, completing request ' + requestId;
447 }
448
449 // Reset metaConnect request
450 _metaConnectRequest = null;
451 }
452
453 function _complete(request, success)
454 {
455 var index = org.cometd.Utils.inArray(request, _requests);
456 // The index can be negative if the request has been aborted
457 if (index >= 0)
458 {
459 _requests.splice(index, 1);
460 }
461
462 if (_envelopes.length > 0)
463 {
464 var envelopeAndRequest = _envelopes.shift();
465 var nextEnvelope = envelopeAndRequest[0];
466 var nextRequest = envelopeAndRequest[1];
467 this._debug('Transport dequeued request', nextRequest.id);
468 if (success)
469 {
470 if (this.getConfiguration().autoBatch)
471 {
472 _coalesceEnvelopes.call(this, nextEnvelope);
473 }
474 _queueSend.call(this, nextEnvelope);
475 this._debug('Transport completed request', request.id, nextEnvelope);
476 }
477 else
478 {
479 // Keep the semantic of calling response callbacks asynchronously after the request
480 var self = this;
481 this.setTimeout(function()
482 {
483 self.complete(nextRequest, false, nextRequest.metaConnect);
484 nextEnvelope.onFailure(nextRequest.xhr, nextEnvelope.messages, 'error', 'Previous request failed');
485 }, 0);
486 }
487 }
488 }
489
490 _self.complete = function(request, success, metaConnect)
491 {
492 if (metaConnect)
493 {
494 _metaConnectComplete.call(this, request);
495 }
496 else
497 {
498 _complete.call(this, request, success);
499 }
500 };
501
502 /**
503 * Performs the actual send depending on the transport type details.
504 * @param envelope the envelope to send
505 * @param request the request information
506 */
507 _self.transportSend = function(envelope, request)
508 {
509 throw 'Abstract';
510 };
511
512 _self.transportSuccess = function(envelope, request, responses)
513 {
514 if (!request.expired)
515 {
516 this.clearTimeout(request.timeout);
517 this.complete(request, true, request.metaConnect);
518 if (responses && responses.length > 0)
519 {
520 envelope.onSuccess(responses);
521 }
522 else
523 {
524 envelope.onFailure(request.xhr, envelope.messages, 'Empty HTTP response');
525 }
526 }
527 };
528
529 _self.transportFailure = function(envelope, request, reason, exception)
530 {
531 if (!request.expired)
532 {
533 this.clearTimeout(request.timeout);
534 this.complete(request, false, request.metaConnect);
535 envelope.onFailure(request.xhr, envelope.messages, reason, exception);
536 }
537 };
538
539 function _metaConnectSend(envelope)
540 {
541 if (_metaConnectRequest !== null)
542 {
543 throw 'Concurrent metaConnect requests not allowed, request id=' + _metaConnectRequest.id + ' not yet completed';
544 }
545
546 var requestId = ++_requestIds;
547 this._debug('Transport', this.getType(), 'metaConnect send, request', requestId, 'envelope', envelope);
548 var request = {
549 id: requestId,
550 metaConnect: true
551 };
552 _transportSend.call(this, envelope, request);
553 _metaConnectRequest = request;
554 }
555
556 _self.send = function(envelope, metaConnect)
557 {
558 if (metaConnect)
559 {
560 _metaConnectSend.call(this, envelope);
561 }
562 else
563 {
564 _queueSend.call(this, envelope);
565 }
566 };
567
568 _self.abort = function()
569 {
570 _super.abort();
571 for (var i = 0; i < _requests.length; ++i)
572 {
573 var request = _requests[i];
574 this._debug('Aborting request', request);
575 if (request.xhr)
576 {
577 request.xhr.abort();
578 }
579 }
580 if (_metaConnectRequest)
581 {
582 this._debug('Aborting metaConnect request', _metaConnectRequest);
583 if (_metaConnectRequest.xhr)
584 {
585 _metaConnectRequest.xhr.abort();
586 }
587 }
588 this.reset();
589 };
590
591 _self.reset = function()
592 {
593 _super.reset();
594 _metaConnectRequest = null;
595 _requests = [];
596 _envelopes = [];
597 };
598
599 return _self;
600};
601
602org.cometd.LongPollingTransport = function()
603{
604 var _super = new org.cometd.RequestTransport();
605 var _self = org.cometd.Transport.derive(_super);
606 // By default, support cross domain
607 var _supportsCrossDomain = true;
608
609 _self.accept = function(version, crossDomain, url)
610 {
611 return _supportsCrossDomain || !crossDomain;
612 };
613
614 _self.xhrSend = function(packet)
615 {
616 throw 'Abstract';
617 };
618
619 _self.transportSend = function(envelope, request)
620 {
621 this._debug('Transport', this.getType(), 'sending request', request.id, 'envelope', envelope);
622
623 var self = this;
624 try
625 {
626 var sameStack = true;
627 request.xhr = this.xhrSend({
628 transport: this,
629 url: envelope.url,
630 sync: envelope.sync,
631 headers: this.getConfiguration().requestHeaders,
632 body: org.cometd.JSON.toJSON(envelope.messages),
633 onSuccess: function(response)
634 {
635 self._debug('Transport', self.getType(), 'received response', response);
636 var success = false;
637 try
638 {
639 var received = self.convertToMessages(response);
640 if (received.length === 0)
641 {
642 _supportsCrossDomain = false;
643 self.transportFailure(envelope, request, 'no response', null);
644 }
645 else
646 {
647 success = true;
648 self.transportSuccess(envelope, request, received);
649 }
650 }
651 catch(x)
652 {
653 self._debug(x);
654 if (!success)
655 {
656 _supportsCrossDomain = false;
657 self.transportFailure(envelope, request, 'bad response', x);
658 }
659 }
660 },
661 onError: function(reason, exception)
662 {
663 _supportsCrossDomain = false;
664 if (sameStack)
665 {
666 // Keep the semantic of calling response callbacks asynchronously after the request
667 self.setTimeout(function()
668 {
669 self.transportFailure(envelope, request, reason, exception);
670 }, 0);
671 }
672 else
673 {
674 self.transportFailure(envelope, request, reason, exception);
675 }
676 }
677 });
678 sameStack = false;
679 }
680 catch (x)
681 {
682 _supportsCrossDomain = false;
683 // Keep the semantic of calling response callbacks asynchronously after the request
684 this.setTimeout(function()
685 {
686 self.transportFailure(envelope, request, 'error', x);
687 }, 0);
688 }
689 };
690
691 _self.reset = function()
692 {
693 _super.reset();
694 _supportsCrossDomain = true;
695 };
696
697 return _self;
698};
699
700org.cometd.CallbackPollingTransport = function()
701{
702 var _super = new org.cometd.RequestTransport();
703 var _self = org.cometd.Transport.derive(_super);
704 var _maxLength = 2000;
705
706 _self.accept = function(version, crossDomain, url)
707 {
708 return true;
709 };
710
711 _self.jsonpSend = function(packet)
712 {
713 throw 'Abstract';
714 };
715
716 _self.transportSend = function(envelope, request)
717 {
718 var self = this;
719
720 // Microsoft Internet Explorer has a 2083 URL max length
721 // We must ensure that we stay within that length
722 var start = 0;
723 var length = envelope.messages.length;
724 var lengths = [];
725 while (length > 0)
726 {
727 // Encode the messages because all brackets, quotes, commas, colons, etc
728 // present in the JSON will be URL encoded, taking many more characters
729 var json = org.cometd.JSON.toJSON(envelope.messages.slice(start, start + length));
730 var urlLength = envelope.url.length + encodeURI(json).length;
731
732 // Let's stay on the safe side and use 2000 instead of 2083
733 // also because we did not count few characters among which
734 // the parameter name 'message' and the parameter 'jsonp',
735 // which sum up to about 50 chars
736 if (urlLength > _maxLength)
737 {
738 if (length === 1)
739 {
740 var x = 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
741 'for transport ' + this.getType();
742 // Keep the semantic of calling response callbacks asynchronously after the request
743 this.setTimeout(function()
744 {
745 self.transportFailure(envelope, request, 'error', x);
746 }, 0);
747 return;
748 }
749
750 --length;
751 continue;
752 }
753
754 lengths.push(length);
755 start += length;
756 length = envelope.messages.length - start;
757 }
758
759 // Here we are sure that the messages can be sent within the URL limit
760
761 var envelopeToSend = envelope;
762 if (lengths.length > 1)
763 {
764 var begin = 0;
765 var end = lengths[0];
766 this._debug('Transport', this.getType(), 'split', envelope.messages.length, 'messages into', lengths.join(' + '));
767 envelopeToSend = this._mixin(false, {}, envelope);
768 envelopeToSend.messages = envelope.messages.slice(begin, end);
769 envelopeToSend.onSuccess = envelope.onSuccess;
770 envelopeToSend.onFailure = envelope.onFailure;
771
772 for (var i = 1; i < lengths.length; ++i)
773 {
774 var nextEnvelope = this._mixin(false, {}, envelope);
775 begin = end;
776 end += lengths[i];
777 nextEnvelope.messages = envelope.messages.slice(begin, end);
778 nextEnvelope.onSuccess = envelope.onSuccess;
779 nextEnvelope.onFailure = envelope.onFailure;
780 this.send(nextEnvelope, request.metaConnect);
781 }
782 }
783
784 this._debug('Transport', this.getType(), 'sending request', request.id, 'envelope', envelopeToSend);
785
786 try
787 {
788 var sameStack = true;
789 this.jsonpSend({
790 transport: this,
791 url: envelopeToSend.url,
792 sync: envelopeToSend.sync,
793 headers: this.getConfiguration().requestHeaders,
794 body: org.cometd.JSON.toJSON(envelopeToSend.messages),
795 onSuccess: function(responses)
796 {
797 var success = false;
798 try
799 {
800 var received = self.convertToMessages(responses);
801 if (received.length === 0)
802 {
803 self.transportFailure(envelopeToSend, request, 'no response');
804 }
805 else
806 {
807 success=true;
808 self.transportSuccess(envelopeToSend, request, received);
809 }
810 }
811 catch (x)
812 {
813 self._debug(x);
814 if (!success)
815 {
816 self.transportFailure(envelopeToSend, request, 'bad response', x);
817 }
818 }
819 },
820 onError: function(reason, exception)
821 {
822 if (sameStack)
823 {
824 // Keep the semantic of calling response callbacks asynchronously after the request
825 self.setTimeout(function()
826 {
827 self.transportFailure(envelopeToSend, request, reason, exception);
828 }, 0);
829 }
830 else
831 {
832 self.transportFailure(envelopeToSend, request, reason, exception);
833 }
834 }
835 });
836 sameStack = false;
837 }
838 catch (xx)
839 {
840 // Keep the semantic of calling response callbacks asynchronously after the request
841 this.setTimeout(function()
842 {
843 self.transportFailure(envelopeToSend, request, 'error', xx);
844 }, 0);
845 }
846 };
847
848 return _self;
849};
850
851org.cometd.WebSocketTransport = function()
852{
853 var _super = new org.cometd.Transport();
854 var _self = org.cometd.Transport.derive(_super);
855 var _cometd;
856 // By default, support WebSocket
857 var _supportsWebSocket = true;
858 // Whether we were able to establish a WebSocket connection
859 var _webSocketSupported = false;
860 // Envelopes that have been sent
861 var _envelopes = {};
862 // Timeouts for messages that have been sent
863 var _timeouts = {};
864 var _webSocket = null;
865 var _opened = false;
866 var _connected = false;
867 var _successCallback;
868
869 function _websocketConnect()
870 {
871 // Mangle the URL, changing the scheme from 'http' to 'ws'
872 var url = _cometd.getURL().replace(/^http/, 'ws');
873 this._debug('Transport', this.getType(), 'connecting to URL', url);
874
875 var self = this;
876 var connectTimer = null;
877
878 var connectTimeout = _cometd.getConfiguration().connectTimeout;
879 if (connectTimeout > 0)
880 {
881 connectTimer = this.setTimeout(function()
882 {
883 connectTimer = null;
884 if (!_opened)
885 {
886 self._debug('Transport', self.getType(), 'timed out while connecting to URL', url, ':', connectTimeout, 'ms');
887 self.onClose(1002, 'Connect Timeout');
888 }
889 }, connectTimeout);
890 }
891
892 var webSocket = new org.cometd.WebSocket(url);
893 webSocket.onopen = function()
894 {
895 self._debug('WebSocket opened', webSocket);
896 if (connectTimer)
897 {
898 self.clearTimeout(connectTimer);
899 connectTimer = null;
900 }
901 if (webSocket !== _webSocket)
902 {
903 // It's possible that the onopen callback is invoked
904 // with a delay so that we have already reconnected
905 self._debug('Ignoring open event, WebSocket', _webSocket);
906 return;
907 }
908 self.onOpen();
909 };
910 webSocket.onclose = function(event)
911 {
912 var code = event ? event.code : 1000;
913 var reason = event ? event.reason : undefined;
914 self._debug('WebSocket closed', code, '/', reason, webSocket);
915 if (connectTimer)
916 {
917 self.clearTimeout(connectTimer);
918 connectTimer = null;
919 }
920 if (webSocket !== _webSocket)
921 {
922 // The onclose callback may be invoked when the server sends
923 // the close message reply, but after we have already reconnected
924 self._debug('Ignoring close event, WebSocket', _webSocket);
925 return;
926 }
927 self.onClose(code, reason);
928 };
929 webSocket.onerror = function()
930 {
931 webSocket.onclose({ code: 1002 });
932 };
933 webSocket.onmessage = function(message)
934 {
935 self._debug('WebSocket message', message, webSocket);
936 if (webSocket !== _webSocket)
937 {
938 self._debug('Ignoring message event, WebSocket', _webSocket);
939 return;
940 }
941 self.onMessage(message);
942 };
943
944 _webSocket = webSocket;
945 this._debug('Transport', this.getType(), 'configured callbacks on', webSocket);
946 }
947
948 function _webSocketSend(envelope, metaConnect)
949 {
950 var json = org.cometd.JSON.toJSON(envelope.messages);
951
952 _webSocket.send(json);
953 this._debug('Transport', this.getType(), 'sent', envelope, 'metaConnect =', metaConnect);
954
955 // Manage the timeout waiting for the response
956 var maxDelay = this.getConfiguration().maxNetworkDelay;
957 var delay = maxDelay;
958 if (metaConnect)
959 {
960 delay += this.getAdvice().timeout;
961 _connected = true;
962 }
963
964 var messageIds = [];
965 for (var i = 0; i < envelope.messages.length; ++i)
966 {
967 var message = envelope.messages[i];
968 if (message.id)
969 {
970 messageIds.push(message.id);
971 var self = this;
972 _timeouts[message.id] = this.setTimeout(function()
973 {
974 if (_webSocket)
975 {
976 _webSocket.close(1000, 'Timeout');
977 }
978 }, delay);
979 }
980 }
981
982 this._debug('Transport', this.getType(), 'waiting at most', delay, 'ms for messages', messageIds, 'maxNetworkDelay', maxDelay, ', timeouts:', _timeouts);
983 }
984
985 function _send(envelope, metaConnect)
986 {
987 try
988 {
989 if (_webSocket === null)
990 {
991 _websocketConnect.call(this);
992 }
993 // We may have a non-null _webSocket, but not be open yet so
994 // to avoid out of order deliveries, we check if we are open
995 else if (_opened)
996 {
997 _webSocketSend.call(this, envelope, metaConnect);
998 }
999 }
1000 catch (x)
1001 {
1002 // Keep the semantic of calling response callbacks asynchronously after the request
1003 this.setTimeout(function()
1004 {
1005 envelope.onFailure(_webSocket, envelope.messages, 'error', x);
1006 }, 0);
1007 }
1008 }
1009
1010 _self.onOpen = function()
1011 {
1012 this._debug('Transport', this.getType(), 'opened', _webSocket);
1013 _opened = true;
1014 _webSocketSupported = true;
1015
1016 this._debug('Sending pending messages', _envelopes);
1017 for (var key in _envelopes)
1018 {
1019 var element = _envelopes[key];
1020 var envelope = element[0];
1021 var metaConnect = element[1];
1022 // Store the success callback, which is independent from the envelope,
1023 // so that it can be used to notify arrival of messages.
1024 _successCallback = envelope.onSuccess;
1025 _webSocketSend.call(this, envelope, metaConnect);
1026 }
1027 };
1028
1029 _self.onMessage = function(wsMessage)
1030 {
1031 this._debug('Transport', this.getType(), 'received websocket message', wsMessage, _webSocket);
1032
1033 var close = false;
1034 var messages = this.convertToMessages(wsMessage.data);
1035 var messageIds = [];
1036 for (var i = 0; i < messages.length; ++i)
1037 {
1038 var message = messages[i];
1039
1040 // Detect if the message is a response to a request we made.
1041 // If it's a meta message, for sure it's a response;
1042 // otherwise it's a publish message and publish responses lack the data field
1043 if (/^\/meta\//.test(message.channel) || message.data === undefined)
1044 {
1045 if (message.id)
1046 {
1047 messageIds.push(message.id);
1048
1049 var timeout = _timeouts[message.id];
1050 if (timeout)
1051 {
1052 this.clearTimeout(timeout);
1053 delete _timeouts[message.id];
1054 this._debug('Transport', this.getType(), 'removed timeout for message', message.id, ', timeouts', _timeouts);
1055 }
1056 }
1057 }
1058
1059 if ('/meta/connect' === message.channel)
1060 {
1061 _connected = false;
1062 }
1063 if ('/meta/disconnect' === message.channel && !_connected)
1064 {
1065 close = true;
1066 }
1067 }
1068
1069 // Remove the envelope corresponding to the messages
1070 var removed = false;
1071 for (var j = 0; j < messageIds.length; ++j)
1072 {
1073 var id = messageIds[j];
1074 for (var key in _envelopes)
1075 {
1076 var ids = key.split(',');
1077 var index = org.cometd.Utils.inArray(id, ids);
1078 if (index >= 0)
1079 {
1080 removed = true;
1081 ids.splice(index, 1);
1082 var envelope = _envelopes[key][0];
1083 var metaConnect = _envelopes[key][1];
1084 delete _envelopes[key];
1085 if (ids.length > 0)
1086 {
1087 _envelopes[ids.join(',')] = [envelope, metaConnect];
1088 }
1089 break;
1090 }
1091 }
1092 }
1093 if (removed)
1094 {
1095 this._debug('Transport', this.getType(), 'removed envelope, envelopes', _envelopes);
1096 }
1097
1098 _successCallback.call(this, messages);
1099
1100 if (close)
1101 {
1102 _webSocket.close(1000, 'Disconnect');
1103 }
1104 };
1105
1106 _self.onClose = function(code, reason)
1107 {
1108 this._debug('Transport', this.getType(), 'closed', code, reason, _webSocket);
1109
1110 // Remember if we were able to connect
1111 // This close event could be due to server shutdown, and if it restarts we want to try websocket again
1112 _supportsWebSocket = _webSocketSupported;
1113
1114 for (var id in _timeouts)
1115 {
1116 this.clearTimeout(_timeouts[id]);
1117 }
1118 _timeouts = {};
1119
1120 for (var key in _envelopes)
1121 {
1122 var envelope = _envelopes[key][0];
1123 var metaConnect = _envelopes[key][1];
1124 if (metaConnect)
1125 {
1126 _connected = false;
1127 }
1128 envelope.onFailure(_webSocket, envelope.messages, 'closed ' + code + '/' + reason);
1129 }
1130 _envelopes = {};
1131
1132 if (_webSocket !== null && _opened)
1133 {
1134 _webSocket.close(1000, 'Close');
1135 }
1136 _opened = false;
1137 _webSocket = null;
1138 };
1139
1140 _self.registered = function(type, cometd)
1141 {
1142 _super.registered(type, cometd);
1143 _cometd = cometd;
1144 };
1145
1146 _self.accept = function(version, crossDomain, url)
1147 {
1148 // Using !! to return a boolean (and not the WebSocket object)
1149 return _supportsWebSocket && !!org.cometd.WebSocket && _cometd.websocketEnabled !== false;
1150 };
1151
1152 _self.send = function(envelope, metaConnect)
1153 {
1154 this._debug('Transport', this.getType(), 'sending', envelope, 'metaConnect =', metaConnect);
1155
1156 // Store the envelope in any case; if the websocket cannot be opened, we fail it in close()
1157 var messageIds = [];
1158 for (var i = 0; i < envelope.messages.length; ++i)
1159 {
1160 var message = envelope.messages[i];
1161 if (message.id)
1162 {
1163 messageIds.push(message.id);
1164 }
1165 }
1166 _envelopes[messageIds.join(',')] = [envelope, metaConnect];
1167 this._debug('Transport', this.getType(), 'stored envelope, envelopes', _envelopes);
1168
1169 _send.call(this, envelope, metaConnect);
1170 };
1171
1172 _self.reset = function()
1173 {
1174 _super.reset();
1175 if (_webSocket !== null && _opened)
1176 {
1177 _webSocket.close(1000, 'Reset');
1178 }
1179 _supportsWebSocket = true;
1180 _webSocketSupported = false;
1181 _timeouts = {};
1182 _envelopes = {};
1183 _webSocket = null;
1184 _opened = false;
1185 _successCallback = null;
1186 };
1187
1188 return _self;
1189};
1190
1191/**
1192 * The constructor for a Cometd object, identified by an optional name.
1193 * The default name is the string 'default'.
1194 * In the rare case a page needs more than one Bayeux conversation,
1195 * a new instance can be created via:
1196 * <pre>
1197 * var bayeuxUrl2 = ...;
1198 *
1199 * // Dojo style
1200 * var cometd2 = new dojox.Cometd('another_optional_name');
1201 *
1202 * // jQuery style
1203 * var cometd2 = new $.Cometd('another_optional_name');
1204 *
1205 * cometd2.init({url: bayeuxUrl2});
1206 * </pre>
1207 * @param name the optional name of this cometd object
1208 */
1209// IMPLEMENTATION NOTES:
1210// Be very careful in not changing the function order and pass this file every time through JSLint (http://jslint.com)
1211// The only implied globals must be "dojo", "org" and "window", and check that there are no "unused" warnings
1212// Failing to pass JSLint may result in shrinkers/minifiers to create an unusable file.
1213org.cometd.Cometd = function(name)
1214{
1215 var _cometd = this;
1216 var _name = name || 'default';
1217 var _crossDomain = false;
1218 var _transports = new org.cometd.TransportRegistry();
1219 var _transport;
1220 var _status = 'disconnected';
1221 var _messageId = 0;
1222 var _clientId = null;
1223 var _batch = 0;
1224 var _messageQueue = [];
1225 var _internalBatch = false;
1226 var _listeners = {};
1227 var _backoff = 0;
1228 var _scheduledSend = null;
1229 var _extensions = [];
1230 var _advice = {};
1231 var _handshakeProps;
1232 var _reestablish = false;
1233 var _connected = false;
1234 var _config = {
1235 connectTimeout: 0,
1236 maxConnections: 2,
1237 backoffIncrement: 1000,
1238 maxBackoff: 60000,
1239 logLevel: 'info',
1240 reverseIncomingExtensions: true,
1241 maxNetworkDelay: 10000,
1242 requestHeaders: {},
1243 appendMessageTypeToURL: true,
1244 autoBatch: false,
1245 advice: {
1246 timeout: 60000,
1247 interval: 0,
1248 reconnect: 'retry'
1249 }
1250 };
1251
1252 /**
1253 * Mixes in the given objects into the target object by copying the properties.
1254 * @param deep if the copy must be deep
1255 * @param target the target object
1256 * @param objects the objects whose properties are copied into the target
1257 */
1258 this._mixin = function(deep, target, objects)
1259 {
1260 var result = target || {};
1261
1262 // Skip first 2 parameters (deep and target), and loop over the others
1263 for (var i = 2; i < arguments.length; ++i)
1264 {
1265 var object = arguments[i];
1266
1267 if (object === undefined || object === null)
1268 {
1269 continue;
1270 }
1271
1272 for (var propName in object)
1273 {
1274 var prop = object[propName];
1275 var targ = result[propName];
1276
1277 // Avoid infinite loops
1278 if (prop === target)
1279 {
1280 continue;
1281 }
1282 // Do not mixin undefined values
1283 if (prop === undefined)
1284 {
1285 continue;
1286 }
1287
1288 if (deep && typeof prop === 'object' && prop !== null)
1289 {
1290 if (prop instanceof Array)
1291 {
1292 result[propName] = this._mixin(deep, targ instanceof Array ? targ : [], prop);
1293 }
1294 else
1295 {
1296 var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {};
1297 result[propName] = this._mixin(deep, source, prop);
1298 }
1299 }
1300 else
1301 {
1302 result[propName] = prop;
1303 }
1304 }
1305 }
1306
1307 return result;
1308 };
1309
1310 function _isString(value)
1311 {
1312 return org.cometd.Utils.isString(value);
1313 }
1314
1315 function _isFunction(value)
1316 {
1317 if (value === undefined || value === null)
1318 {
1319 return false;
1320 }
1321 return typeof value === 'function';
1322 }
1323
1324 function _log(level, args)
1325 {
1326 if (window.console)
1327 {
1328 var logger = window.console[level];
1329 if (_isFunction(logger))
1330 {
1331 logger.apply(window.console, args);
1332 }
1333 }
1334 }
1335
1336 this._warn = function()
1337 {
1338 _log('warn', arguments);
1339 };
1340
1341 this._info = function()
1342 {
1343 if (_config.logLevel !== 'warn')
1344 {
1345 _log('info', arguments);
1346 }
1347 };
1348
1349 this._debug = function()
1350 {
1351 if (_config.logLevel === 'debug')
1352 {
1353 _log('debug', arguments);
1354 }
1355 };
1356
1357 /**
1358 * Returns whether the given hostAndPort is cross domain.
1359 * The default implementation checks against window.location.host
1360 * but this function can be overridden to make it work in non-browser
1361 * environments.
1362 *
1363 * @param hostAndPort the host and port in format host:port
1364 * @return whether the given hostAndPort is cross domain
1365 */
1366 this._isCrossDomain = function(hostAndPort)
1367 {
1368 return hostAndPort && hostAndPort !== window.location.host;
1369 };
1370
1371 function _configure(configuration)
1372 {
1373 _cometd._debug('Configuring cometd object with', configuration);
1374 // Support old style param, where only the Bayeux server URL was passed
1375 if (_isString(configuration))
1376 {
1377 configuration = { url: configuration };
1378 }
1379 if (!configuration)
1380 {
1381 configuration = {};
1382 }
1383
1384 _config = _cometd._mixin(false, _config, configuration);
1385
1386 if (!_config.url)
1387 {
1388 throw 'Missing required configuration parameter \'url\' specifying the Bayeux server URL';
1389 }
1390
1391 // Check if we're cross domain
1392 // [1] = protocol://, [2] = host:port, [3] = host, [4] = IPv6_host, [5] = IPv4_host, [6] = :port, [7] = port, [8] = uri, [9] = rest
1393 var urlParts = /(^https?:\/\/)?(((\[[^\]]+\])|([^:\/\?#]+))(:(\d+))?)?([^\?#]*)(.*)?/.exec(_config.url);
1394 var hostAndPort = urlParts[2];
1395 var uri = urlParts[8];
1396 var afterURI = urlParts[9];
1397 _crossDomain = _cometd._isCrossDomain(hostAndPort);
1398
1399 // Check if appending extra path is supported
1400 if (_config.appendMessageTypeToURL)
1401 {
1402 if (afterURI !== undefined && afterURI.length > 0)
1403 {
1404 _cometd._info('Appending message type to URI ' + uri + afterURI + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
1405 _config.appendMessageTypeToURL = false;
1406 }
1407 else
1408 {
1409 var uriSegments = uri.split('/');
1410 var lastSegmentIndex = uriSegments.length - 1;
1411 if (uri.match(/\/$/))
1412 {
1413 lastSegmentIndex -= 1;
1414 }
1415 if (uriSegments[lastSegmentIndex].indexOf('.') >= 0)
1416 {
1417 // Very likely the CometD servlet's URL pattern is mapped to an extension, such as *.cometd
1418 // It will be difficult to add the extra path in this case
1419 _cometd._info('Appending message type to URI ' + uri + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
1420 _config.appendMessageTypeToURL = false;
1421 }
1422 }
1423 }
1424 }
1425
1426 function _clearSubscriptions()
1427 {
1428 for (var channel in _listeners)
1429 {
1430 var subscriptions = _listeners[channel];
1431 for (var i = 0; i < subscriptions.length; ++i)
1432 {
1433 var subscription = subscriptions[i];
1434 if (subscription && !subscription.listener)
1435 {
1436 delete subscriptions[i];
1437 _cometd._debug('Removed subscription', subscription, 'for channel', channel);
1438 }
1439 }
1440 }
1441 }
1442
1443 function _setStatus(newStatus)
1444 {
1445 if (_status !== newStatus)
1446 {
1447 _cometd._debug('Status', _status, '->', newStatus);
1448 _status = newStatus;
1449 }
1450 }
1451
1452 function _isDisconnected()
1453 {
1454 return _status === 'disconnecting' || _status === 'disconnected';
1455 }
1456
1457 function _nextMessageId()
1458 {
1459 return ++_messageId;
1460 }
1461
1462 function _applyExtension(scope, callback, name, message, outgoing)
1463 {
1464 try
1465 {
1466 return callback.call(scope, message);
1467 }
1468 catch (x)
1469 {
1470 _cometd._debug('Exception during execution of extension', name, x);
1471 var exceptionCallback = _cometd.onExtensionException;
1472 if (_isFunction(exceptionCallback))
1473 {
1474 _cometd._debug('Invoking extension exception callback', name, x);
1475 try
1476 {
1477 exceptionCallback.call(_cometd, x, name, outgoing, message);
1478 }
1479 catch(xx)
1480 {
1481 _cometd._info('Exception during execution of exception callback in extension', name, xx);
1482 }
1483 }
1484 return message;
1485 }
1486 }
1487
1488 function _applyIncomingExtensions(message)
1489 {
1490 for (var i = 0; i < _extensions.length; ++i)
1491 {
1492 if (message === undefined || message === null)
1493 {
1494 break;
1495 }
1496
1497 var index = _config.reverseIncomingExtensions ? _extensions.length - 1 - i : i;
1498 var extension = _extensions[index];
1499 var callback = extension.extension.incoming;
1500 if (_isFunction(callback))
1501 {
1502 var result = _applyExtension(extension.extension, callback, extension.name, message, false);
1503 message = result === undefined ? message : result;
1504 }
1505 }
1506 return message;
1507 }
1508
1509 function _applyOutgoingExtensions(message)
1510 {
1511 for (var i = 0; i < _extensions.length; ++i)
1512 {
1513 if (message === undefined || message === null)
1514 {
1515 break;
1516 }
1517
1518 var extension = _extensions[i];
1519 var callback = extension.extension.outgoing;
1520 if (_isFunction(callback))
1521 {
1522 var result = _applyExtension(extension.extension, callback, extension.name, message, true);
1523 message = result === undefined ? message : result;
1524 }
1525 }
1526 return message;
1527 }
1528
1529 function _notify(channel, message)
1530 {
1531 var subscriptions = _listeners[channel];
1532 if (subscriptions && subscriptions.length > 0)
1533 {
1534 for (var i = 0; i < subscriptions.length; ++i)
1535 {
1536 var subscription = subscriptions[i];
1537 // Subscriptions may come and go, so the array may have 'holes'
1538 if (subscription)
1539 {
1540 try
1541 {
1542 subscription.callback.call(subscription.scope, message);
1543 }
1544 catch (x)
1545 {
1546 _cometd._debug('Exception during notification', subscription, message, x);
1547 var listenerCallback = _cometd.onListenerException;
1548 if (_isFunction(listenerCallback))
1549 {
1550 _cometd._debug('Invoking listener exception callback', subscription, x);
1551 try
1552 {
1553 listenerCallback.call(_cometd, x, subscription.handle, subscription.listener, message);
1554 }
1555 catch (xx)
1556 {
1557 _cometd._info('Exception during execution of listener callback', subscription, xx);
1558 }
1559 }
1560 }
1561 }
1562 }
1563 }
1564 }
1565
1566 function _notifyListeners(channel, message)
1567 {
1568 // Notify direct listeners
1569 _notify(channel, message);
1570
1571 // Notify the globbing listeners
1572 var channelParts = channel.split('/');
1573 var last = channelParts.length - 1;
1574 for (var i = last; i > 0; --i)
1575 {
1576 var channelPart = channelParts.slice(0, i).join('/') + '/*';
1577 // We don't want to notify /foo/* if the channel is /foo/bar/baz,
1578 // so we stop at the first non recursive globbing
1579 if (i === last)
1580 {
1581 _notify(channelPart, message);
1582 }
1583 // Add the recursive globber and notify
1584 channelPart += '*';
1585 _notify(channelPart, message);
1586 }
1587 }
1588
1589 function _cancelDelayedSend()
1590 {
1591 if (_scheduledSend !== null)
1592 {
1593 org.cometd.Utils.clearTimeout(_scheduledSend);
1594 }
1595 _scheduledSend = null;
1596 }
1597
1598 function _delayedSend(operation)
1599 {
1600 _cancelDelayedSend();
1601 var delay = _advice.interval + _backoff;
1602 _cometd._debug('Function scheduled in', delay, 'ms, interval =', _advice.interval, 'backoff =', _backoff, operation);
1603 _scheduledSend = org.cometd.Utils.setTimeout(_cometd, operation, delay);
1604 }
1605
1606 // Needed to break cyclic dependencies between function definitions
1607 var _handleMessages;
1608 var _handleFailure;
1609
1610 /**
1611 * Delivers the messages to the CometD server
1612 * @param messages the array of messages to send
1613 * @param longpoll true if this send is a long poll
1614 */
1615 function _send(sync, messages, longpoll, extraPath)
1616 {
1617 // We must be sure that the messages have a clientId.
1618 // This is not guaranteed since the handshake may take time to return
1619 // (and hence the clientId is not known yet) and the application
1620 // may create other messages.
1621 for (var i = 0; i < messages.length; ++i)
1622 {
1623 var message = messages[i];
1624 message.id = '' + _nextMessageId();
1625 if (_clientId)
1626 {
1627 message.clientId = _clientId;
1628 }
1629 message = _applyOutgoingExtensions(message);
1630 if (message !== undefined && message !== null)
1631 {
1632 messages[i] = message;
1633 }
1634 else
1635 {
1636 messages.splice(i--, 1);
1637 }
1638 }
1639 if (messages.length === 0)
1640 {
1641 return;
1642 }
1643
1644 var url = _config.url;
1645 if (_config.appendMessageTypeToURL)
1646 {
1647 // If url does not end with '/', then append it
1648 if (!url.match(/\/$/))
1649 {
1650 url = url + '/';
1651 }
1652 if (extraPath)
1653 {
1654 url = url + extraPath;
1655 }
1656 }
1657
1658 var envelope = {
1659 url: url,
1660 sync: sync,
1661 messages: messages,
1662 onSuccess: function(rcvdMessages)
1663 {
1664 try
1665 {
1666 _handleMessages.call(_cometd, rcvdMessages);
1667 }
1668 catch (x)
1669 {
1670 _cometd._debug('Exception during handling of messages', x);
1671 }
1672 },
1673 onFailure: function(conduit, messages, reason, exception)
1674 {
1675 try
1676 {
1677 _handleFailure.call(_cometd, conduit, messages, reason, exception);
1678 }
1679 catch (x)
1680 {
1681 _cometd._debug('Exception during handling of failure', x);
1682 }
1683 }
1684 };
1685 _cometd._debug('Send', envelope);
1686 _transport.send(envelope, longpoll);
1687 }
1688
1689 function _queueSend(message)
1690 {
1691 if (_batch > 0 || _internalBatch === true)
1692 {
1693 _messageQueue.push(message);
1694 }
1695 else
1696 {
1697 _send(false, [message], false);
1698 }
1699 }
1700
1701 /**
1702 * Sends a complete bayeux message.
1703 * This method is exposed as a public so that extensions may use it
1704 * to send bayeux message directly, for example in case of re-sending
1705 * messages that have already been sent but that for some reason must
1706 * be resent.
1707 */
1708 this.send = _queueSend;
1709
1710 function _resetBackoff()
1711 {
1712 _backoff = 0;
1713 }
1714
1715 function _increaseBackoff()
1716 {
1717 if (_backoff < _config.maxBackoff)
1718 {
1719 _backoff += _config.backoffIncrement;
1720 }
1721 }
1722
1723 /**
1724 * Starts a the batch of messages to be sent in a single request.
1725 * @see #_endBatch(sendMessages)
1726 */
1727 function _startBatch()
1728 {
1729 ++_batch;
1730 }
1731
1732 function _flushBatch()
1733 {
1734 var messages = _messageQueue;
1735 _messageQueue = [];
1736 if (messages.length > 0)
1737 {
1738 _send(false, messages, false);
1739 }
1740 }
1741
1742 /**
1743 * Ends the batch of messages to be sent in a single request,
1744 * optionally sending messages present in the message queue depending
1745 * on the given argument.
1746 * @see #_startBatch()
1747 */
1748 function _endBatch()
1749 {
1750 --_batch;
1751 if (_batch < 0)
1752 {
1753 throw 'Calls to startBatch() and endBatch() are not paired';
1754 }
1755
1756 if (_batch === 0 && !_isDisconnected() && !_internalBatch)
1757 {
1758 _flushBatch();
1759 }
1760 }
1761
1762 /**
1763 * Sends the connect message
1764 */
1765 function _connect()
1766 {
1767 if (!_isDisconnected())
1768 {
1769 var message = {
1770 channel: '/meta/connect',
1771 connectionType: _transport.getType()
1772 };
1773
1774 // In case of reload or temporary loss of connection
1775 // we want the next successful connect to return immediately
1776 // instead of being held by the server, so that connect listeners
1777 // can be notified that the connection has been re-established
1778 if (!_connected)
1779 {
1780 message.advice = { timeout: 0 };
1781 }
1782
1783 _setStatus('connecting');
1784 _cometd._debug('Connect sent', message);
1785 _send(false, [message], true, 'connect');
1786 _setStatus('connected');
1787 }
1788 }
1789
1790 function _delayedConnect()
1791 {
1792 _setStatus('connecting');
1793 _delayedSend(function()
1794 {
1795 _connect();
1796 });
1797 }
1798
1799 function _updateAdvice(newAdvice)
1800 {
1801 if (newAdvice)
1802 {
1803 _advice = _cometd._mixin(false, {}, _config.advice, newAdvice);
1804 _cometd._debug('New advice', _advice);
1805 }
1806 }
1807
1808 function _disconnect(abort)
1809 {
1810 _cancelDelayedSend();
1811 if (abort)
1812 {
1813 _transport.abort();
1814 }
1815 _clientId = null;
1816 _setStatus('disconnected');
1817 _batch = 0;
1818 _resetBackoff();
1819
1820 // Fail any existing queued message
1821 if (_messageQueue.length > 0)
1822 {
1823 _handleFailure.call(_cometd, undefined, _messageQueue, 'error', 'Disconnected');
1824 _messageQueue = [];
1825 }
1826 }
1827
1828 /**
1829 * Sends the initial handshake message
1830 */
1831 function _handshake(handshakeProps)
1832 {
1833 _clientId = null;
1834
1835 _clearSubscriptions();
1836
1837 // Reset the transports if we're not retrying the handshake
1838 if (_isDisconnected())
1839 {
1840 _transports.reset();
1841 _updateAdvice(_config.advice);
1842 }
1843 else
1844 {
1845 // We are retrying the handshake, either because another handshake failed
1846 // and we're backing off, or because the server timed us out and asks us to
1847 // re-handshake: in both cases, make sure that if the handshake succeeds
1848 // the next action is a connect.
1849 _updateAdvice(_cometd._mixin(false, _advice, {reconnect: 'retry'}));
1850 }
1851
1852 _batch = 0;
1853
1854 // Mark the start of an internal batch.
1855 // This is needed because handshake and connect are async.
1856 // It may happen that the application calls init() then subscribe()
1857 // and the subscribe message is sent before the connect message, if
1858 // the subscribe message is not held until the connect message is sent.
1859 // So here we start a batch to hold temporarily any message until
1860 // the connection is fully established.
1861 _internalBatch = true;
1862
1863 // Save the properties provided by the user, so that
1864 // we can reuse them during automatic re-handshake
1865 _handshakeProps = handshakeProps;
1866
1867 var version = '1.0';
1868
1869 // Figure out the transports to send to the server
1870 var transportTypes = _transports.findTransportTypes(version, _crossDomain, _config.url);
1871
1872 var bayeuxMessage = {
1873 version: version,
1874 minimumVersion: '0.9',
1875 channel: '/meta/handshake',
1876 supportedConnectionTypes: transportTypes,
1877 advice: {
1878 timeout: _advice.timeout,
1879 interval: _advice.interval
1880 }
1881 };
1882 // Do not allow the user to mess with the required properties,
1883 // so merge first the user properties and *then* the bayeux message
1884 var message = _cometd._mixin(false, {}, _handshakeProps, bayeuxMessage);
1885
1886 // Pick up the first available transport as initial transport
1887 // since we don't know if the server supports it
1888 _transport = _transports.negotiateTransport(transportTypes, version, _crossDomain, _config.url);
1889 _cometd._debug('Initial transport is', _transport.getType());
1890
1891 // We started a batch to hold the application messages,
1892 // so here we must bypass it and send immediately.
1893 _setStatus('handshaking');
1894 _cometd._debug('Handshake sent', message);
1895 _send(false, [message], false, 'handshake');
1896 }
1897
1898 function _delayedHandshake()
1899 {
1900 _setStatus('handshaking');
1901
1902 // We will call _handshake() which will reset _clientId, but we want to avoid
1903 // that between the end of this method and the call to _handshake() someone may
1904 // call publish() (or other methods that call _queueSend()).
1905 _internalBatch = true;
1906
1907 _delayedSend(function()
1908 {
1909 _handshake(_handshakeProps);
1910 });
1911 }
1912
1913 function _failHandshake(message)
1914 {
1915 _notifyListeners('/meta/handshake', message);
1916 _notifyListeners('/meta/unsuccessful', message);
1917
1918 // Only try again if we haven't been disconnected and
1919 // the advice permits us to retry the handshake
1920 var retry = !_isDisconnected() && _advice.reconnect !== 'none';
1921 if (retry)
1922 {
1923 _increaseBackoff();
1924 _delayedHandshake();
1925 }
1926 else
1927 {
1928 _disconnect(false);
1929 }
1930 }
1931
1932 function _handshakeResponse(message)
1933 {
1934 if (message.successful)
1935 {
1936 // Save clientId, figure out transport, then follow the advice to connect
1937 _clientId = message.clientId;
1938
1939 var newTransport = _transports.negotiateTransport(message.supportedConnectionTypes, message.version, _crossDomain, _config.url);
1940 if (newTransport === null)
1941 {
1942 throw 'Could not negotiate transport with server; client ' +
1943 _transports.findTransportTypes(message.version, _crossDomain, _config.url) +
1944 ', server ' + message.supportedConnectionTypes;
1945 }
1946 else if (_transport !== newTransport)
1947 {
1948 _cometd._debug('Transport', _transport, '->', newTransport);
1949 _transport = newTransport;
1950 }
1951
1952 // End the internal batch and allow held messages from the application
1953 // to go to the server (see _handshake() where we start the internal batch).
1954 _internalBatch = false;
1955 _flushBatch();
1956
1957 // Here the new transport is in place, as well as the clientId, so
1958 // the listeners can perform a publish() if they want.
1959 // Notify the listeners before the connect below.
1960 message.reestablish = _reestablish;
1961 _reestablish = true;
1962 _notifyListeners('/meta/handshake', message);
1963
1964 var action = _isDisconnected() ? 'none' : _advice.reconnect;
1965 switch (action)
1966 {
1967 case 'retry':
1968 _resetBackoff();
1969 _delayedConnect();
1970 break;
1971 case 'none':
1972 _disconnect(false);
1973 break;
1974 default:
1975 throw 'Unrecognized advice action ' + action;
1976 }
1977 }
1978 else
1979 {
1980 _failHandshake(message);
1981 }
1982 }
1983
1984 function _handshakeFailure(xhr, message)
1985 {
1986 _failHandshake({
1987 successful: false,
1988 failure: true,
1989 channel: '/meta/handshake',
1990 request: message,
1991 xhr: xhr,
1992 advice: {
1993 reconnect: 'retry',
1994 interval: _backoff
1995 }
1996 });
1997 }
1998
1999 function _failConnect(message)
2000 {
2001 // Notify the listeners after the status change but before the next action
2002 _notifyListeners('/meta/connect', message);
2003 _notifyListeners('/meta/unsuccessful', message);
2004
2005 // This may happen when the server crashed, the current clientId
2006 // will be invalid, and the server will ask to handshake again
2007 // Listeners can call disconnect(), so check the state after they run
2008 var action = _isDisconnected() ? 'none' : _advice.reconnect;
2009 switch (action)
2010 {
2011 case 'retry':
2012 _delayedConnect();
2013 _increaseBackoff();
2014 break;
2015 case 'handshake':
2016 // The current transport may be failed (e.g. network disconnection)
2017 // Reset the transports so the new handshake picks up the right one
2018 _transports.reset();
2019 _resetBackoff();
2020 _delayedHandshake();
2021 break;
2022 case 'none':
2023 _disconnect(false);
2024 break;
2025 default:
2026 throw 'Unrecognized advice action' + action;
2027 }
2028 }
2029
2030 function _connectResponse(message)
2031 {
2032 _connected = message.successful;
2033
2034 if (_connected)
2035 {
2036 _notifyListeners('/meta/connect', message);
2037
2038 // Normally, the advice will say "reconnect: 'retry', interval: 0"
2039 // and the server will hold the request, so when a response returns
2040 // we immediately call the server again (long polling)
2041 // Listeners can call disconnect(), so check the state after they run
2042 var action = _isDisconnected() ? 'none' : _advice.reconnect;
2043 switch (action)
2044 {
2045 case 'retry':
2046 _resetBackoff();
2047 _delayedConnect();
2048 break;
2049 case 'none':
2050 _disconnect(false);
2051 break;
2052 default:
2053 throw 'Unrecognized advice action ' + action;
2054 }
2055 }
2056 else
2057 {
2058 _failConnect(message);
2059 }
2060 }
2061
2062 function _connectFailure(xhr, message)
2063 {
2064 _connected = false;
2065 _failConnect({
2066 successful: false,
2067 failure: true,
2068 channel: '/meta/connect',
2069 request: message,
2070 xhr: xhr,
2071 advice: {
2072 reconnect: 'retry',
2073 interval: _backoff
2074 }
2075 });
2076 }
2077
2078 function _failDisconnect(message)
2079 {
2080 _disconnect(true);
2081 _notifyListeners('/meta/disconnect', message);
2082 _notifyListeners('/meta/unsuccessful', message);
2083 }
2084
2085 function _disconnectResponse(message)
2086 {
2087 if (message.successful)
2088 {
2089 _disconnect(false);
2090 _notifyListeners('/meta/disconnect', message);
2091 }
2092 else
2093 {
2094 _failDisconnect(message);
2095 }
2096 }
2097
2098 function _disconnectFailure(xhr, message)
2099 {
2100 _failDisconnect({
2101 successful: false,
2102 failure: true,
2103 channel: '/meta/disconnect',
2104 request: message,
2105 xhr: xhr,
2106 advice: {
2107 reconnect: 'none',
2108 interval: 0
2109 }
2110 });
2111 }
2112
2113 function _failSubscribe(message)
2114 {
2115 _notifyListeners('/meta/subscribe', message);
2116 _notifyListeners('/meta/unsuccessful', message);
2117 }
2118
2119 function _subscribeResponse(message)
2120 {
2121 if (message.successful)
2122 {
2123 _notifyListeners('/meta/subscribe', message);
2124 }
2125 else
2126 {
2127 _failSubscribe(message);
2128 }
2129 }
2130
2131 function _subscribeFailure(xhr, message)
2132 {
2133 _failSubscribe({
2134 successful: false,
2135 failure: true,
2136 channel: '/meta/subscribe',
2137 request: message,
2138 xhr: xhr,
2139 advice: {
2140 reconnect: 'none',
2141 interval: 0
2142 }
2143 });
2144 }
2145
2146 function _failUnsubscribe(message)
2147 {
2148 _notifyListeners('/meta/unsubscribe', message);
2149 _notifyListeners('/meta/unsuccessful', message);
2150 }
2151
2152 function _unsubscribeResponse(message)
2153 {
2154 if (message.successful)
2155 {
2156 _notifyListeners('/meta/unsubscribe', message);
2157 }
2158 else
2159 {
2160 _failUnsubscribe(message);
2161 }
2162 }
2163
2164 function _unsubscribeFailure(xhr, message)
2165 {
2166 _failUnsubscribe({
2167 successful: false,
2168 failure: true,
2169 channel: '/meta/unsubscribe',
2170 request: message,
2171 xhr: xhr,
2172 advice: {
2173 reconnect: 'none',
2174 interval: 0
2175 }
2176 });
2177 }
2178
2179 function _failMessage(message)
2180 {
2181 _notifyListeners('/meta/publish', message);
2182 _notifyListeners('/meta/unsuccessful', message);
2183 }
2184
2185 function _messageResponse(message)
2186 {
2187 if (message.successful === undefined)
2188 {
2189 if (message.data)
2190 {
2191 // It is a plain message, and not a bayeux meta message
2192 _notifyListeners(message.channel, message);
2193 }
2194 else
2195 {
2196 _cometd._debug('Unknown message', message);
2197 }
2198 }
2199 else
2200 {
2201 if (message.successful)
2202 {
2203 _notifyListeners('/meta/publish', message);
2204 }
2205 else
2206 {
2207 _failMessage(message);
2208 }
2209 }
2210 }
2211
2212 function _messageFailure(xhr, message)
2213 {
2214 _failMessage({
2215 successful: false,
2216 failure: true,
2217 channel: message.channel,
2218 request: message,
2219 xhr: xhr,
2220 advice: {
2221 reconnect: 'none',
2222 interval: 0
2223 }
2224 });
2225 }
2226
2227 function _receive(message)
2228 {
2229 message = _applyIncomingExtensions(message);
2230 if (message === undefined || message === null)
2231 {
2232 return;
2233 }
2234
2235 _updateAdvice(message.advice);
2236
2237 var channel = message.channel;
2238 switch (channel)
2239 {
2240 case '/meta/handshake':
2241 _handshakeResponse(message);
2242 break;
2243 case '/meta/connect':
2244 _connectResponse(message);
2245 break;
2246 case '/meta/disconnect':
2247 _disconnectResponse(message);
2248 break;
2249 case '/meta/subscribe':
2250 _subscribeResponse(message);
2251 break;
2252 case '/meta/unsubscribe':
2253 _unsubscribeResponse(message);
2254 break;
2255 default:
2256 _messageResponse(message);
2257 break;
2258 }
2259 }
2260
2261 /**
2262 * Receives a message.
2263 * This method is exposed as a public so that extensions may inject
2264 * messages simulating that they had been received.
2265 */
2266 this.receive = _receive;
2267
2268 _handleMessages = function(rcvdMessages)
2269 {
2270 _cometd._debug('Received', rcvdMessages);
2271
2272 for (var i = 0; i < rcvdMessages.length; ++i)
2273 {
2274 var message = rcvdMessages[i];
2275 _receive(message);
2276 }
2277 };
2278
2279 _handleFailure = function(conduit, messages, reason, exception)
2280 {
2281 _cometd._debug('handleFailure', conduit, messages, reason, exception);
2282
2283 for (var i = 0; i < messages.length; ++i)
2284 {
2285 var message = messages[i];
2286 var channel = message.channel;
2287 switch (channel)
2288 {
2289 case '/meta/handshake':
2290 _handshakeFailure(conduit, message);
2291 break;
2292 case '/meta/connect':
2293 _connectFailure(conduit, message);
2294 break;
2295 case '/meta/disconnect':
2296 _disconnectFailure(conduit, message);
2297 break;
2298 case '/meta/subscribe':
2299 _subscribeFailure(conduit, message);
2300 break;
2301 case '/meta/unsubscribe':
2302 _unsubscribeFailure(conduit, message);
2303 break;
2304 default:
2305 _messageFailure(conduit, message);
2306 break;
2307 }
2308 }
2309 };
2310
2311 function _hasSubscriptions(channel)
2312 {
2313 var subscriptions = _listeners[channel];
2314 if (subscriptions)
2315 {
2316 for (var i = 0; i < subscriptions.length; ++i)
2317 {
2318 if (subscriptions[i])
2319 {
2320 return true;
2321 }
2322 }
2323 }
2324 return false;
2325 }
2326
2327 function _resolveScopedCallback(scope, callback)
2328 {
2329 var delegate = {
2330 scope: scope,
2331 method: callback
2332 };
2333 if (_isFunction(scope))
2334 {
2335 delegate.scope = undefined;
2336 delegate.method = scope;
2337 }
2338 else
2339 {
2340 if (_isString(callback))
2341 {
2342 if (!scope)
2343 {
2344 throw 'Invalid scope ' + scope;
2345 }
2346 delegate.method = scope[callback];
2347 if (!_isFunction(delegate.method))
2348 {
2349 throw 'Invalid callback ' + callback + ' for scope ' + scope;
2350 }
2351 }
2352 else if (!_isFunction(callback))
2353 {
2354 throw 'Invalid callback ' + callback;
2355 }
2356 }
2357 return delegate;
2358 }
2359
2360 function _addListener(channel, scope, callback, isListener)
2361 {
2362 // The data structure is a map<channel, subscription[]>, where each subscription
2363 // holds the callback to be called and its scope.
2364
2365 var delegate = _resolveScopedCallback(scope, callback);
2366 _cometd._debug('Adding listener on', channel, 'with scope', delegate.scope, 'and callback', delegate.method);
2367
2368 var subscription = {
2369 channel: channel,
2370 scope: delegate.scope,
2371 callback: delegate.method,
2372 listener: isListener
2373 };
2374
2375 var subscriptions = _listeners[channel];
2376 if (!subscriptions)
2377 {
2378 subscriptions = [];
2379 _listeners[channel] = subscriptions;
2380 }
2381
2382 // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
2383 // Note that if:
2384 // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
2385 // then:
2386 // hc==3, a.join()=='a',,'c', a.length==3
2387 var subscriptionID = subscriptions.push(subscription) - 1;
2388 subscription.id = subscriptionID;
2389 subscription.handle = [channel, subscriptionID];
2390
2391 _cometd._debug('Added listener', subscription, 'for channel', channel, 'having id =', subscriptionID);
2392
2393 // The subscription to allow removal of the listener is made of the channel and the index
2394 return subscription.handle;
2395 }
2396
2397 function _removeListener(subscription)
2398 {
2399 var subscriptions = _listeners[subscription[0]];
2400 if (subscriptions)
2401 {
2402 delete subscriptions[subscription[1]];
2403 _cometd._debug('Removed listener', subscription);
2404 }
2405 }
2406
2407 //
2408 // PUBLIC API
2409 //
2410
2411 /**
2412 * Registers the given transport under the given transport type.
2413 * The optional index parameter specifies the "priority" at which the
2414 * transport is registered (where 0 is the max priority).
2415 * If a transport with the same type is already registered, this function
2416 * does nothing and returns false.
2417 * @param type the transport type
2418 * @param transport the transport object
2419 * @param index the index at which this transport is to be registered
2420 * @return true if the transport has been registered, false otherwise
2421 * @see #unregisterTransport(type)
2422 */
2423 this.registerTransport = function(type, transport, index)
2424 {
2425 var result = _transports.add(type, transport, index);
2426 if (result)
2427 {
2428 this._debug('Registered transport', type);
2429
2430 if (_isFunction(transport.registered))
2431 {
2432 transport.registered(type, this);
2433 }
2434 }
2435 return result;
2436 };
2437
2438 /**
2439 * @return an array of all registered transport types
2440 */
2441 this.getTransportTypes = function()
2442 {
2443 return _transports.getTransportTypes();
2444 };
2445
2446 /**
2447 * Unregisters the transport with the given transport type.
2448 * @param type the transport type to unregister
2449 * @return the transport that has been unregistered,
2450 * or null if no transport was previously registered under the given transport type
2451 */
2452 this.unregisterTransport = function(type)
2453 {
2454 var transport = _transports.remove(type);
2455 if (transport !== null)
2456 {
2457 this._debug('Unregistered transport', type);
2458
2459 if (_isFunction(transport.unregistered))
2460 {
2461 transport.unregistered();
2462 }
2463 }
2464 return transport;
2465 };
2466
2467 this.unregisterTransports = function()
2468 {
2469 _transports.clear();
2470 };
2471
2472 this.findTransport = function(name)
2473 {
2474 return _transports.find(name);
2475 };
2476
2477 /**
2478 * Configures the initial Bayeux communication with the Bayeux server.
2479 * Configuration is passed via an object that must contain a mandatory field <code>url</code>
2480 * of type string containing the URL of the Bayeux server.
2481 * @param configuration the configuration object
2482 */
2483 this.configure = function(configuration)
2484 {
2485 _configure.call(this, configuration);
2486 };
2487
2488 /**
2489 * Configures and establishes the Bayeux communication with the Bayeux server
2490 * via a handshake and a subsequent connect.
2491 * @param configuration the configuration object
2492 * @param handshakeProps an object to be merged with the handshake message
2493 * @see #configure(configuration)
2494 * @see #handshake(handshakeProps)
2495 */
2496 this.init = function(configuration, handshakeProps)
2497 {
2498 this.configure(configuration);
2499 this.handshake(handshakeProps);
2500 };
2501
2502 /**
2503 * Establishes the Bayeux communication with the Bayeux server
2504 * via a handshake and a subsequent connect.
2505 * @param handshakeProps an object to be merged with the handshake message
2506 */
2507 this.handshake = function(handshakeProps)
2508 {
2509 _setStatus('disconnected');
2510 _reestablish = false;
2511 _handshake(handshakeProps);
2512 };
2513
2514 /**
2515 * Disconnects from the Bayeux server.
2516 * It is possible to suggest to attempt a synchronous disconnect, but this feature
2517 * may only be available in certain transports (for example, long-polling may support
2518 * it, callback-polling certainly does not).
2519 * @param sync whether attempt to perform a synchronous disconnect
2520 * @param disconnectProps an object to be merged with the disconnect message
2521 */
2522 this.disconnect = function(sync, disconnectProps)
2523 {
2524 if (_isDisconnected())
2525 {
2526 return;
2527 }
2528
2529 if (disconnectProps === undefined)
2530 {
2531 if (typeof sync !== 'boolean')
2532 {
2533 disconnectProps = sync;
2534 sync = false;
2535 }
2536 }
2537
2538 var bayeuxMessage = {
2539 channel: '/meta/disconnect'
2540 };
2541 var message = this._mixin(false, {}, disconnectProps, bayeuxMessage);
2542 _setStatus('disconnecting');
2543 _send(sync === true, [message], false, 'disconnect');
2544 };
2545
2546 /**
2547 * Marks the start of a batch of application messages to be sent to the server
2548 * in a single request, obtaining a single response containing (possibly) many
2549 * application reply messages.
2550 * Messages are held in a queue and not sent until {@link #endBatch()} is called.
2551 * If startBatch() is called multiple times, then an equal number of endBatch()
2552 * calls must be made to close and send the batch of messages.
2553 * @see #endBatch()
2554 */
2555 this.startBatch = function()
2556 {
2557 _startBatch();
2558 };
2559
2560 /**
2561 * Marks the end of a batch of application messages to be sent to the server
2562 * in a single request.
2563 * @see #startBatch()
2564 */
2565 this.endBatch = function()
2566 {
2567 _endBatch();
2568 };
2569
2570 /**
2571 * Executes the given callback in the given scope, surrounded by a {@link #startBatch()}
2572 * and {@link #endBatch()} calls.
2573 * @param scope the scope of the callback, may be omitted
2574 * @param callback the callback to be executed within {@link #startBatch()} and {@link #endBatch()} calls
2575 */
2576 this.batch = function(scope, callback)
2577 {
2578 var delegate = _resolveScopedCallback(scope, callback);
2579 this.startBatch();
2580 try
2581 {
2582 delegate.method.call(delegate.scope);
2583 this.endBatch();
2584 }
2585 catch (x)
2586 {
2587 this._debug('Exception during execution of batch', x);
2588 this.endBatch();
2589 throw x;
2590 }
2591 };
2592
2593 /**
2594 * Adds a listener for bayeux messages, performing the given callback in the given scope
2595 * when a message for the given channel arrives.
2596 * @param channel the channel the listener is interested to
2597 * @param scope the scope of the callback, may be omitted
2598 * @param callback the callback to call when a message is sent to the channel
2599 * @returns the subscription handle to be passed to {@link #removeListener(object)}
2600 * @see #removeListener(subscription)
2601 */
2602 this.addListener = function(channel, scope, callback)
2603 {
2604 if (arguments.length < 2)
2605 {
2606 throw 'Illegal arguments number: required 2, got ' + arguments.length;
2607 }
2608 if (!_isString(channel))
2609 {
2610 throw 'Illegal argument type: channel must be a string';
2611 }
2612
2613 return _addListener(channel, scope, callback, true);
2614 };
2615
2616 /**
2617 * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
2618 * @param subscription the subscription to unsubscribe.
2619 * @see #addListener(channel, scope, callback)
2620 */
2621 this.removeListener = function(subscription)
2622 {
2623 if (!org.cometd.Utils.isArray(subscription))
2624 {
2625 throw 'Invalid argument: expected subscription, not ' + subscription;
2626 }
2627
2628 _removeListener(subscription);
2629 };
2630
2631 /**
2632 * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
2633 * {@link #subscribe(channel, scope, callback)}.
2634 */
2635 this.clearListeners = function()
2636 {
2637 _listeners = {};
2638 };
2639
2640 /**
2641 * Subscribes to the given channel, performing the given callback in the given scope
2642 * when a message for the channel arrives.
2643 * @param channel the channel to subscribe to
2644 * @param scope the scope of the callback, may be omitted
2645 * @param callback the callback to call when a message is sent to the channel
2646 * @param subscribeProps an object to be merged with the subscribe message
2647 * @return the subscription handle to be passed to {@link #unsubscribe(object)}
2648 */
2649 this.subscribe = function(channel, scope, callback, subscribeProps)
2650 {
2651 if (arguments.length < 2)
2652 {
2653 throw 'Illegal arguments number: required 2, got ' + arguments.length;
2654 }
2655 if (!_isString(channel))
2656 {
2657 throw 'Illegal argument type: channel must be a string';
2658 }
2659 if (_isDisconnected())
2660 {
2661 throw 'Illegal state: already disconnected';
2662 }
2663
2664 // Normalize arguments
2665 if (_isFunction(scope))
2666 {
2667 subscribeProps = callback;
2668 callback = scope;
2669 scope = undefined;
2670 }
2671
2672 // Only send the message to the server if this client has not yet subscribed to the channel
2673 var send = !_hasSubscriptions(channel);
2674
2675 var subscription = _addListener(channel, scope, callback, false);
2676
2677 if (send)
2678 {
2679 // Send the subscription message after the subscription registration to avoid
2680 // races where the server would send a message to the subscribers, but here
2681 // on the client the subscription has not been added yet to the data structures
2682 var bayeuxMessage = {
2683 channel: '/meta/subscribe',
2684 subscription: channel
2685 };
2686 var message = this._mixin(false, {}, subscribeProps, bayeuxMessage);
2687 _queueSend(message);
2688 }
2689
2690 return subscription;
2691 };
2692
2693 /**
2694 * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
2695 * @param subscription the subscription to unsubscribe.
2696 */
2697 this.unsubscribe = function(subscription, unsubscribeProps)
2698 {
2699 if (arguments.length < 1)
2700 {
2701 throw 'Illegal arguments number: required 1, got ' + arguments.length;
2702 }
2703 if (_isDisconnected())
2704 {
2705 throw 'Illegal state: already disconnected';
2706 }
2707
2708 // Remove the local listener before sending the message
2709 // This ensures that if the server fails, this client does not get notifications
2710 this.removeListener(subscription);
2711
2712 var channel = subscription[0];
2713 // Only send the message to the server if this client unsubscribes the last subscription
2714 if (!_hasSubscriptions(channel))
2715 {
2716 var bayeuxMessage = {
2717 channel: '/meta/unsubscribe',
2718 subscription: channel
2719 };
2720 var message = this._mixin(false, {}, unsubscribeProps, bayeuxMessage);
2721 _queueSend(message);
2722 }
2723 };
2724
2725 /**
2726 * Removes all subscriptions added via {@link #subscribe(channel, scope, callback, subscribeProps)},
2727 * but does not remove the listeners added via {@link addListener(channel, scope, callback)}.
2728 */
2729 this.clearSubscriptions = function()
2730 {
2731 _clearSubscriptions();
2732 };
2733
2734 /**
2735 * Publishes a message on the given channel, containing the given content.
2736 * @param channel the channel to publish the message to
2737 * @param content the content of the message
2738 * @param publishProps an object to be merged with the publish message
2739 */
2740 this.publish = function(channel, content, publishProps)
2741 {
2742 if (arguments.length < 1)
2743 {
2744 throw 'Illegal arguments number: required 1, got ' + arguments.length;
2745 }
2746 if (!_isString(channel))
2747 {
2748 throw 'Illegal argument type: channel must be a string';
2749 }
2750 if (_isDisconnected())
2751 {
2752 throw 'Illegal state: already disconnected';
2753 }
2754
2755 var bayeuxMessage = {
2756 channel: channel,
2757 data: content
2758 };
2759 var message = this._mixin(false, {}, publishProps, bayeuxMessage);
2760 _queueSend(message);
2761 };
2762
2763 /**
2764 * Returns a string representing the status of the bayeux communication with the Bayeux server.
2765 */
2766 this.getStatus = function()
2767 {
2768 return _status;
2769 };
2770
2771 /**
2772 * Returns whether this instance has been disconnected.
2773 */
2774 this.isDisconnected = _isDisconnected;
2775
2776 /**
2777 * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
2778 * Default value is 1 second, which means if there is a persistent failure the retries will happen
2779 * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
2780 * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
2781 * @param period the backoff period to set
2782 * @see #getBackoffIncrement()
2783 */
2784 this.setBackoffIncrement = function(period)
2785 {
2786 _config.backoffIncrement = period;
2787 };
2788
2789 /**
2790 * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
2791 * @see #setBackoffIncrement(period)
2792 */
2793 this.getBackoffIncrement = function()
2794 {
2795 return _config.backoffIncrement;
2796 };
2797
2798 /**
2799 * Returns the backoff period to wait before retrying an unsuccessful or failed message.
2800 */
2801 this.getBackoffPeriod = function()
2802 {
2803 return _backoff;
2804 };
2805
2806 /**
2807 * Sets the log level for console logging.
2808 * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
2809 * less verbose to more verbose.
2810 * @param level the log level string
2811 */
2812 this.setLogLevel = function(level)
2813 {
2814 _config.logLevel = level;
2815 };
2816
2817 /**
2818 * Registers an extension whose callbacks are called for every incoming message
2819 * (that comes from the server to this client implementation) and for every
2820 * outgoing message (that originates from this client implementation for the
2821 * server).
2822 * The format of the extension object is the following:
2823 * <pre>
2824 * {
2825 * incoming: function(message) { ... },
2826 * outgoing: function(message) { ... }
2827 * }
2828 * </pre>
2829 * Both properties are optional, but if they are present they will be called
2830 * respectively for each incoming message and for each outgoing message.
2831 * @param name the name of the extension
2832 * @param extension the extension to register
2833 * @return true if the extension was registered, false otherwise
2834 * @see #unregisterExtension(name)
2835 */
2836 this.registerExtension = function(name, extension)
2837 {
2838 if (arguments.length < 2)
2839 {
2840 throw 'Illegal arguments number: required 2, got ' + arguments.length;
2841 }
2842 if (!_isString(name))
2843 {
2844 throw 'Illegal argument type: extension name must be a string';
2845 }
2846
2847 var existing = false;
2848 for (var i = 0; i < _extensions.length; ++i)
2849 {
2850 var existingExtension = _extensions[i];
2851 if (existingExtension.name === name)
2852 {
2853 existing = true;
2854 break;
2855 }
2856 }
2857 if (!existing)
2858 {
2859 _extensions.push({
2860 name: name,
2861 extension: extension
2862 });
2863 this._debug('Registered extension', name);
2864
2865 // Callback for extensions
2866 if (_isFunction(extension.registered))
2867 {
2868 extension.registered(name, this);
2869 }
2870
2871 return true;
2872 }
2873 else
2874 {
2875 this._info('Could not register extension with name', name, 'since another extension with the same name already exists');
2876 return false;
2877 }
2878 };
2879
2880 /**
2881 * Unregister an extension previously registered with
2882 * {@link #registerExtension(name, extension)}.
2883 * @param name the name of the extension to unregister.
2884 * @return true if the extension was unregistered, false otherwise
2885 */
2886 this.unregisterExtension = function(name)
2887 {
2888 if (!_isString(name))
2889 {
2890 throw 'Illegal argument type: extension name must be a string';
2891 }
2892
2893 var unregistered = false;
2894 for (var i = 0; i < _extensions.length; ++i)
2895 {
2896 var extension = _extensions[i];
2897 if (extension.name === name)
2898 {
2899 _extensions.splice(i, 1);
2900 unregistered = true;
2901 this._debug('Unregistered extension', name);
2902
2903 // Callback for extensions
2904 var ext = extension.extension;
2905 if (_isFunction(ext.unregistered))
2906 {
2907 ext.unregistered();
2908 }
2909
2910 break;
2911 }
2912 }
2913 return unregistered;
2914 };
2915
2916 /**
2917 * Find the extension registered with the given name.
2918 * @param name the name of the extension to find
2919 * @return the extension found or null if no extension with the given name has been registered
2920 */
2921 this.getExtension = function(name)
2922 {
2923 for (var i = 0; i < _extensions.length; ++i)
2924 {
2925 var extension = _extensions[i];
2926 if (extension.name === name)
2927 {
2928 return extension.extension;
2929 }
2930 }
2931 return null;
2932 };
2933
2934 /**
2935 * Returns the name assigned to this Cometd object, or the string 'default'
2936 * if no name has been explicitly passed as parameter to the constructor.
2937 */
2938 this.getName = function()
2939 {
2940 return _name;
2941 };
2942
2943 /**
2944 * Returns the clientId assigned by the Bayeux server during handshake.
2945 */
2946 this.getClientId = function()
2947 {
2948 return _clientId;
2949 };
2950
2951 /**
2952 * Returns the URL of the Bayeux server.
2953 */
2954 this.getURL = function()
2955 {
2956 return _config.url;
2957 };
2958
2959 this.getTransport = function()
2960 {
2961 return _transport;
2962 };
2963
2964 this.getConfiguration = function()
2965 {
2966 return this._mixin(true, {}, _config);
2967 };
2968
2969 this.getAdvice = function()
2970 {
2971 return this._mixin(true, {}, _advice);
2972 };
2973
2974 // WebSocket handling for Firefox, which deploys WebSocket
2975 // under the name of MozWebSocket in Firefox 6, 7, 8 and 9
2976 org.cometd.WebSocket = window.WebSocket;
2977 if (!org.cometd.WebSocket)
2978 {
2979 org.cometd.WebSocket = window.MozWebSocket;
2980 }
2981};
2982