2 * Copyright (c) 2010 the original author or authors.
\r
4 * Licensed under the Apache License, Version 2.0 (the "License");
\r
5 * you may not use this file except in compliance with the License.
\r
6 * You may obtain a copy of the License at
\r
8 * http://www.apache.org/licenses/LICENSE-2.0
\r
10 * Unless required by applicable law or agreed to in writing, software
\r
11 * distributed under the License is distributed on an "AS IS" BASIS,
\r
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
13 * See the License for the specific language governing permissions and
\r
14 * limitations under the License.
\r
17 // Namespaces for the cometd implementation
\r
18 this.org = this.org || {};
\r
21 org.cometd.JSON = {};
\r
22 org.cometd.JSON.toJSON = org.cometd.JSON.fromJSON = function(object)
\r
27 org.cometd.Utils = {};
\r
29 org.cometd.Utils.isString = function(value)
\r
31 if (value === undefined || value === null)
\r
35 return typeof value === 'string' || value instanceof String;
\r
38 org.cometd.Utils.isArray = function(value)
\r
40 if (value === undefined || value === null)
\r
44 return value instanceof Array;
\r
48 * Returns whether the given element is contained into the given array.
\r
49 * @param element the element to check presence for
\r
50 * @param array the array to check for the element presence
\r
51 * @return the index of the element, if present, or a negative index if the element is not present
\r
53 org.cometd.Utils.inArray = function(element, array)
\r
55 for (var i = 0; i < array.length; ++i)
\r
57 if (element === array[i])
\r
65 org.cometd.Utils.setTimeout = function(cometd, funktion, delay)
\r
67 return window.setTimeout(function()
\r
75 cometd._debug('Exception invoking timed function', funktion, x);
\r
80 org.cometd.Utils.clearTimeout = function(timeoutHandle)
\r
82 window.clearTimeout(timeoutHandle);
\r
86 * A registry for transports used by the Cometd object.
\r
88 org.cometd.TransportRegistry = function()
\r
91 var _transports = {};
\r
93 this.getTransportTypes = function()
\r
95 return _types.slice(0);
\r
98 this.findTransportTypes = function(version, crossDomain, url)
\r
101 for (var i = 0; i < _types.length; ++i)
\r
103 var type = _types[i];
\r
104 if (_transports[type].accept(version, crossDomain, url) === true)
\r
112 this.negotiateTransport = function(types, version, crossDomain, url)
\r
114 for (var i = 0; i < _types.length; ++i)
\r
116 var type = _types[i];
\r
117 for (var j = 0; j < types.length; ++j)
\r
119 if (type === types[j])
\r
121 var transport = _transports[type];
\r
122 if (transport.accept(version, crossDomain, url) === true)
\r
132 this.add = function(type, transport, index)
\r
134 var existing = false;
\r
135 for (var i = 0; i < _types.length; ++i)
\r
137 if (_types[i] === type)
\r
146 if (typeof index !== 'number')
\r
152 _types.splice(index, 0, type);
\r
154 _transports[type] = transport;
\r
160 this.find = function(type)
\r
162 for (var i = 0; i < _types.length; ++i)
\r
164 if (_types[i] === type)
\r
166 return _transports[type];
\r
172 this.remove = function(type)
\r
174 for (var i = 0; i < _types.length; ++i)
\r
176 if (_types[i] === type)
\r
178 _types.splice(i, 1);
\r
179 var transport = _transports[type];
\r
180 delete _transports[type];
\r
187 this.clear = function()
\r
193 this.reset = function()
\r
195 for (var i = 0; i < _types.length; ++i)
\r
197 _transports[_types[i]].reset();
\r
203 * Base object with the common functionality for transports.
\r
205 org.cometd.Transport = function()
\r
211 * Function invoked just after a transport has been successfully registered.
\r
212 * @param type the type of transport (for example 'long-polling')
\r
213 * @param cometd the cometd object this transport has been registered to
\r
214 * @see #unregistered()
\r
216 this.registered = function(type, cometd)
\r
223 * Function invoked just after a transport has been successfully unregistered.
\r
224 * @see #registered(type, cometd)
\r
226 this.unregistered = function()
\r
232 this._debug = function()
\r
234 _cometd._debug.apply(_cometd, arguments);
\r
237 this._mixin = function()
\r
239 return _cometd._mixin.apply(_cometd, arguments);
\r
242 this.getConfiguration = function()
\r
244 return _cometd.getConfiguration();
\r
247 this.getAdvice = function()
\r
249 return _cometd.getAdvice();
\r
252 this.setTimeout = function(funktion, delay)
\r
254 return org.cometd.Utils.setTimeout(_cometd, funktion, delay);
\r
257 this.clearTimeout = function(handle)
\r
259 org.cometd.Utils.clearTimeout(handle);
\r
263 * Converts the given response into an array of bayeux messages
\r
264 * @param response the response to convert
\r
265 * @return an array of bayeux messages obtained by converting the response
\r
267 this.convertToMessages = function (response)
\r
269 if (org.cometd.Utils.isString(response))
\r
273 return org.cometd.JSON.fromJSON(response);
\r
277 this._debug('Could not convert to JSON the following string', '"' + response + '"');
\r
281 if (org.cometd.Utils.isArray(response))
\r
285 if (response === undefined || response === null)
\r
289 if (response instanceof Object)
\r
293 throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
\r
297 * Returns whether this transport can work for the given version and cross domain communication case.
\r
298 * @param version a string indicating the transport version
\r
299 * @param crossDomain a boolean indicating whether the communication is cross domain
\r
300 * @return true if this transport can work for the given version and cross domain communication case,
\r
303 this.accept = function(version, crossDomain, url)
\r
309 * Returns the type of this transport.
\r
310 * @see #registered(type, cometd)
\r
312 this.getType = function()
\r
317 this.send = function(envelope, metaConnect)
\r
322 this.reset = function()
\r
324 this._debug('Transport', _type, 'reset');
\r
327 this.abort = function()
\r
329 this._debug('Transport', _type, 'aborted');
\r
332 this.toString = function()
\r
334 return this.getType();
\r
338 org.cometd.Transport.derive = function(baseObject)
\r
341 F.prototype = baseObject;
\r
346 * Base object with the common functionality for transports based on requests.
\r
347 * The key responsibility is to allow at most 2 outstanding requests to the server,
\r
348 * to avoid that requests are sent behind a long poll.
\r
349 * To achieve this, we have one reserved request for the long poll, and all other
\r
350 * requests are serialized one after the other.
\r
352 org.cometd.RequestTransport = function()
\r
354 var _super = new org.cometd.Transport();
\r
355 var _self = org.cometd.Transport.derive(_super);
\r
356 var _requestIds = 0;
\r
357 var _metaConnectRequest = null;
\r
358 var _requests = [];
\r
359 var _envelopes = [];
\r
361 function _coalesceEnvelopes(envelope)
\r
363 while (_envelopes.length > 0)
\r
365 var envelopeAndRequest = _envelopes[0];
\r
366 var newEnvelope = envelopeAndRequest[0];
\r
367 var newRequest = envelopeAndRequest[1];
\r
368 if (newEnvelope.url === envelope.url &&
\r
369 newEnvelope.sync === envelope.sync)
\r
371 _envelopes.shift();
\r
372 envelope.messages = envelope.messages.concat(newEnvelope.messages);
\r
373 this._debug('Coalesced', newEnvelope.messages.length, 'messages from request', newRequest.id);
\r
380 function _transportSend(envelope, request)
\r
382 this.transportSend(envelope, request);
\r
383 request.expired = false;
\r
385 if (!envelope.sync)
\r
387 var maxDelay = this.getConfiguration().maxNetworkDelay;
\r
388 var delay = maxDelay;
\r
389 if (request.metaConnect === true)
\r
391 delay += this.getAdvice().timeout;
\r
394 this._debug('Transport', this.getType(), 'waiting at most', delay, 'ms for the response, maxNetworkDelay', maxDelay);
\r
397 request.timeout = this.setTimeout(function()
\r
399 request.expired = true;
\r
402 request.xhr.abort();
\r
404 var errorMessage = 'Request ' + request.id + ' of transport ' + self.getType() + ' exceeded ' + delay + ' ms max network delay';
\r
405 self._debug(errorMessage);
\r
406 self.complete(request, false, request.metaConnect);
\r
407 envelope.onFailure(request.xhr, envelope.messages, 'timeout', errorMessage);
\r
412 function _queueSend(envelope)
\r
414 var requestId = ++_requestIds;
\r
420 // Consider the metaConnect requests which should always be present
\r
421 if (_requests.length < this.getConfiguration().maxConnections - 1)
\r
423 _requests.push(request);
\r
424 _transportSend.call(this, envelope, request);
\r
428 this._debug('Transport', this.getType(), 'queueing request', requestId, 'envelope', envelope);
\r
429 _envelopes.push([envelope, request]);
\r
433 function _metaConnectComplete(request)
\r
435 var requestId = request.id;
\r
436 this._debug('Transport', this.getType(), 'metaConnect complete, request', requestId);
\r
437 if (_metaConnectRequest !== null && _metaConnectRequest.id !== requestId)
\r
439 throw 'Longpoll request mismatch, completing request ' + requestId;
\r
442 // Reset metaConnect request
\r
443 _metaConnectRequest = null;
\r
446 function _complete(request, success)
\r
448 var index = org.cometd.Utils.inArray(request, _requests);
\r
449 // The index can be negative if the request has been aborted
\r
452 _requests.splice(index, 1);
\r
455 if (_envelopes.length > 0)
\r
457 var envelopeAndRequest = _envelopes.shift();
\r
458 var nextEnvelope = envelopeAndRequest[0];
\r
459 var nextRequest = envelopeAndRequest[1];
\r
460 this._debug('Transport dequeued request', nextRequest.id);
\r
463 if (this.getConfiguration().autoBatch)
\r
465 _coalesceEnvelopes.call(this, nextEnvelope);
\r
467 _queueSend.call(this, nextEnvelope);
\r
468 this._debug('Transport completed request', request.id, nextEnvelope);
\r
472 // Keep the semantic of calling response callbacks asynchronously after the request
\r
474 this.setTimeout(function()
\r
476 self.complete(nextRequest, false, nextRequest.metaConnect);
\r
477 nextEnvelope.onFailure(nextRequest.xhr, nextEnvelope.messages, 'error', 'Previous request failed');
\r
483 _self.complete = function(request, success, metaConnect)
\r
487 _metaConnectComplete.call(this, request);
\r
491 _complete.call(this, request, success);
\r
496 * Performs the actual send depending on the transport type details.
\r
497 * @param envelope the envelope to send
\r
498 * @param request the request information
\r
500 _self.transportSend = function(envelope, request)
\r
505 _self.transportSuccess = function(envelope, request, responses)
\r
507 if (!request.expired)
\r
509 this.clearTimeout(request.timeout);
\r
510 this.complete(request, true, request.metaConnect);
\r
511 if (responses && responses.length > 0)
\r
513 envelope.onSuccess(responses);
\r
517 envelope.onFailure(request.xhr, envelope.messages, 'Empty HTTP response');
\r
522 _self.transportFailure = function(envelope, request, reason, exception)
\r
524 if (!request.expired)
\r
526 this.clearTimeout(request.timeout);
\r
527 this.complete(request, false, request.metaConnect);
\r
528 envelope.onFailure(request.xhr, envelope.messages, reason, exception);
\r
532 function _metaConnectSend(envelope)
\r
534 if (_metaConnectRequest !== null)
\r
536 throw 'Concurrent metaConnect requests not allowed, request id=' + _metaConnectRequest.id + ' not yet completed';
\r
539 var requestId = ++_requestIds;
\r
540 this._debug('Transport', this.getType(), 'metaConnect send, request', requestId, 'envelope', envelope);
\r
545 _transportSend.call(this, envelope, request);
\r
546 _metaConnectRequest = request;
\r
549 _self.send = function(envelope, metaConnect)
\r
553 _metaConnectSend.call(this, envelope);
\r
557 _queueSend.call(this, envelope);
\r
561 _self.abort = function()
\r
564 for (var i = 0; i < _requests.length; ++i)
\r
566 var request = _requests[i];
\r
567 this._debug('Aborting request', request);
\r
570 request.xhr.abort();
\r
573 if (_metaConnectRequest)
\r
575 this._debug('Aborting metaConnect request', _metaConnectRequest);
\r
576 if (_metaConnectRequest.xhr)
\r
578 _metaConnectRequest.xhr.abort();
\r
584 _self.reset = function()
\r
587 _metaConnectRequest = null;
\r
595 org.cometd.LongPollingTransport = function()
\r
597 var _super = new org.cometd.RequestTransport();
\r
598 var _self = org.cometd.Transport.derive(_super);
\r
599 // By default, support cross domain
\r
600 var _supportsCrossDomain = true;
\r
602 _self.accept = function(version, crossDomain, url)
\r
604 return _supportsCrossDomain || !crossDomain;
\r
607 _self.xhrSend = function(packet)
\r
612 _self.transportSend = function(envelope, request)
\r
614 this._debug('Transport', this.getType(), 'sending request', request.id, 'envelope', envelope);
\r
619 var sameStack = true;
\r
620 request.xhr = this.xhrSend({
\r
623 sync: envelope.sync,
\r
624 headers: this.getConfiguration().requestHeaders,
\r
625 body: org.cometd.JSON.toJSON(envelope.messages),
\r
626 onSuccess: function(response)
\r
628 self._debug('Transport', self.getType(), 'received response', response);
\r
629 var success = false;
\r
632 var received = self.convertToMessages(response);
\r
633 if (received.length === 0)
\r
635 _supportsCrossDomain = false;
\r
636 self.transportFailure(envelope, request, 'no response', null);
\r
641 self.transportSuccess(envelope, request, received);
\r
649 _supportsCrossDomain = false;
\r
650 self.transportFailure(envelope, request, 'bad response', x);
\r
654 onError: function(reason, exception)
\r
656 _supportsCrossDomain = false;
\r
659 // Keep the semantic of calling response callbacks asynchronously after the request
\r
660 self.setTimeout(function()
\r
662 self.transportFailure(envelope, request, reason, exception);
\r
667 self.transportFailure(envelope, request, reason, exception);
\r
675 _supportsCrossDomain = false;
\r
676 // Keep the semantic of calling response callbacks asynchronously after the request
\r
677 this.setTimeout(function()
\r
679 self.transportFailure(envelope, request, 'error', x);
\r
684 _self.reset = function()
\r
687 _supportsCrossDomain = true;
\r
693 org.cometd.CallbackPollingTransport = function()
\r
695 var _super = new org.cometd.RequestTransport();
\r
696 var _self = org.cometd.Transport.derive(_super);
\r
697 var _maxLength = 2000;
\r
699 _self.accept = function(version, crossDomain, url)
\r
704 _self.jsonpSend = function(packet)
\r
709 _self.transportSend = function(envelope, request)
\r
713 // Microsoft Internet Explorer has a 2083 URL max length
\r
714 // We must ensure that we stay within that length
\r
716 var length = envelope.messages.length;
\r
720 // Encode the messages because all brackets, quotes, commas, colons, etc
\r
721 // present in the JSON will be URL encoded, taking many more characters
\r
722 var json = org.cometd.JSON.toJSON(envelope.messages.slice(start, start + length));
\r
723 var urlLength = envelope.url.length + encodeURI(json).length;
\r
725 // Let's stay on the safe side and use 2000 instead of 2083
\r
726 // also because we did not count few characters among which
\r
727 // the parameter name 'message' and the parameter 'jsonp',
\r
728 // which sum up to about 50 chars
\r
729 if (urlLength > _maxLength)
\r
733 var x = 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
\r
734 'for transport ' + this.getType();
\r
735 // Keep the semantic of calling response callbacks asynchronously after the request
\r
736 this.setTimeout(function()
\r
738 self.transportFailure(envelope, request, 'error', x);
\r
747 lengths.push(length);
\r
749 length = envelope.messages.length - start;
\r
752 // Here we are sure that the messages can be sent within the URL limit
\r
754 var envelopeToSend = envelope;
\r
755 if (lengths.length > 1)
\r
758 var end = lengths[0];
\r
759 this._debug('Transport', this.getType(), 'split', envelope.messages.length, 'messages into', lengths.join(' + '));
\r
760 envelopeToSend = this._mixin(false, {}, envelope);
\r
761 envelopeToSend.messages = envelope.messages.slice(begin, end);
\r
762 envelopeToSend.onSuccess = envelope.onSuccess;
\r
763 envelopeToSend.onFailure = envelope.onFailure;
\r
765 for (var i = 1; i < lengths.length; ++i)
\r
767 var nextEnvelope = this._mixin(false, {}, envelope);
\r
770 nextEnvelope.messages = envelope.messages.slice(begin, end);
\r
771 nextEnvelope.onSuccess = envelope.onSuccess;
\r
772 nextEnvelope.onFailure = envelope.onFailure;
\r
773 this.send(nextEnvelope, request.metaConnect);
\r
777 this._debug('Transport', this.getType(), 'sending request', request.id, 'envelope', envelopeToSend);
\r
781 var sameStack = true;
\r
784 url: envelopeToSend.url,
\r
785 sync: envelopeToSend.sync,
\r
786 headers: this.getConfiguration().requestHeaders,
\r
787 body: org.cometd.JSON.toJSON(envelopeToSend.messages),
\r
788 onSuccess: function(responses)
\r
790 var success = false;
\r
793 var received = self.convertToMessages(responses);
\r
794 if (received.length === 0)
\r
796 self.transportFailure(envelopeToSend, request, 'no response');
\r
801 self.transportSuccess(envelopeToSend, request, received);
\r
809 self.transportFailure(envelopeToSend, request, 'bad response', x);
\r
813 onError: function(reason, exception)
\r
817 // Keep the semantic of calling response callbacks asynchronously after the request
\r
818 self.setTimeout(function()
\r
820 self.transportFailure(envelopeToSend, request, reason, exception);
\r
825 self.transportFailure(envelopeToSend, request, reason, exception);
\r
833 // Keep the semantic of calling response callbacks asynchronously after the request
\r
834 this.setTimeout(function()
\r
836 self.transportFailure(envelopeToSend, request, 'error', xx);
\r
844 org.cometd.WebSocketTransport = function()
\r
846 var _super = new org.cometd.Transport();
\r
847 var _self = org.cometd.Transport.derive(_super);
\r
849 // By default, support WebSocket
\r
850 var _supportsWebSocket = true;
\r
851 // Whether we were able to establish a WebSocket connection
\r
852 var _webSocketSupported = false;
\r
853 // Envelopes that have been sent
\r
854 var _envelopes = {};
\r
855 // Timeouts for messages that have been sent
\r
856 var _timeouts = {};
\r
857 var _webSocket = null;
\r
858 var _opened = false;
\r
859 var _connected = false;
\r
860 var _successCallback;
\r
862 function _websocketConnect()
\r
864 // Mangle the URL, changing the scheme from 'http' to 'ws'
\r
865 var url = _cometd.getURL().replace(/^http/, 'ws');
\r
866 this._debug('Transport', this.getType(), 'connecting to URL', url);
\r
869 var connectTimer = null;
\r
871 var connectTimeout = _cometd.getConfiguration().connectTimeout;
\r
872 if (connectTimeout > 0)
\r
874 connectTimer = this.setTimeout(function()
\r
876 connectTimer = null;
\r
879 self._debug('Transport', self.getType(), 'timed out while connecting to URL', url, ':', connectTimeout, 'ms');
\r
880 self.onClose(1002, 'Connect Timeout');
\r
882 }, connectTimeout);
\r
885 var webSocket = new org.cometd.WebSocket(url);
\r
886 var onopen = function()
\r
888 self._debug('WebSocket opened', webSocket);
\r
891 self.clearTimeout(connectTimer);
\r
892 connectTimer = null;
\r
894 if (webSocket !== _webSocket)
\r
896 // It's possible that the onopen callback is invoked
\r
897 // with a delay so that we have already reconnected
\r
898 self._debug('Ignoring open event, WebSocket', _webSocket);
\r
903 var onclose = function(event)
\r
905 var code = event ? event.code : 1000;
\r
906 var reason = event ? event.reason : undefined;
\r
907 self._debug('WebSocket closed', code, '/', reason, webSocket);
\r
910 self.clearTimeout(connectTimer);
\r
911 connectTimer = null;
\r
913 if (webSocket !== _webSocket)
\r
915 // The onclose callback may be invoked when the server sends
\r
916 // the close message reply, but after we have already reconnected
\r
917 self._debug('Ignoring close event, WebSocket', _webSocket);
\r
920 self.onClose(code, reason);
\r
922 var onmessage = function(message)
\r
924 self._debug('WebSocket message', message, webSocket);
\r
925 if (webSocket !== _webSocket)
\r
927 self._debug('Ignoring message event, WebSocket', _webSocket);
\r
930 self.onMessage(message);
\r
933 webSocket.onopen = onopen;
\r
934 webSocket.onclose = onclose;
\r
935 webSocket.onerror = function()
\r
937 onclose({ code: 1002 });
\r
939 webSocket.onmessage = onmessage;
\r
941 _webSocket = webSocket;
\r
942 this._debug('Transport', this.getType(), 'configured callbacks on', webSocket);
\r
945 function _webSocketSend(envelope, metaConnect)
\r
947 var json = org.cometd.JSON.toJSON(envelope.messages);
\r
949 _webSocket.send(json);
\r
950 this._debug('Transport', this.getType(), 'sent', envelope, 'metaConnect =', metaConnect);
\r
952 // Manage the timeout waiting for the response
\r
953 var maxDelay = this.getConfiguration().maxNetworkDelay;
\r
954 var delay = maxDelay;
\r
957 delay += this.getAdvice().timeout;
\r
961 var messageIds = [];
\r
962 for (var i = 0; i < envelope.messages.length; ++i)
\r
964 var message = envelope.messages[i];
\r
967 messageIds.push(message.id);
\r
969 var webSocket = _webSocket;
\r
970 _timeouts[message.id] = this.setTimeout(function()
\r
974 webSocket.close(1000, 'Timeout');
\r
980 this._debug('Transport', this.getType(), 'waiting at most', delay, 'ms for messages', messageIds, 'maxNetworkDelay', maxDelay, ', timeouts:', _timeouts);
\r
983 function _send(envelope, metaConnect)
\r
987 if (_webSocket === null)
\r
989 _websocketConnect.call(this);
\r
991 // We may have a non-null _webSocket, but not be open yet so
\r
992 // to avoid out of order deliveries, we check if we are open
\r
995 _webSocketSend.call(this, envelope, metaConnect);
\r
1000 // Keep the semantic of calling response callbacks asynchronously after the request
\r
1001 var webSocket = _webSocket;
\r
1002 this.setTimeout(function()
\r
1004 envelope.onFailure(webSocket, envelope.messages, 'error', x);
\r
1009 _self.onOpen = function()
\r
1011 this._debug('Transport', this.getType(), 'opened', _webSocket);
\r
1013 _webSocketSupported = true;
\r
1015 this._debug('Sending pending messages', _envelopes);
\r
1016 for (var key in _envelopes)
\r
1018 var element = _envelopes[key];
\r
1019 var envelope = element[0];
\r
1020 var metaConnect = element[1];
\r
1021 // Store the success callback, which is independent from the envelope,
\r
1022 // so that it can be used to notify arrival of messages.
\r
1023 _successCallback = envelope.onSuccess;
\r
1024 _webSocketSend.call(this, envelope, metaConnect);
\r
1028 _self.onMessage = function(wsMessage)
\r
1030 this._debug('Transport', this.getType(), 'received websocket message', wsMessage, _webSocket);
\r
1032 var close = false;
\r
1033 var messages = this.convertToMessages(wsMessage.data);
\r
1034 var messageIds = [];
\r
1035 for (var i = 0; i < messages.length; ++i)
\r
1037 var message = messages[i];
\r
1039 // Detect if the message is a response to a request we made.
\r
1040 // If it's a meta message, for sure it's a response;
\r
1041 // otherwise it's a publish message and publish responses lack the data field
\r
1042 if (/^\/meta\//.test(message.channel) || message.data === undefined)
\r
1046 messageIds.push(message.id);
\r
1048 var timeout = _timeouts[message.id];
\r
1051 this.clearTimeout(timeout);
\r
1052 delete _timeouts[message.id];
\r
1053 this._debug('Transport', this.getType(), 'removed timeout for message', message.id, ', timeouts', _timeouts);
\r
1058 if ('/meta/connect' === message.channel)
\r
1060 _connected = false;
\r
1062 if ('/meta/disconnect' === message.channel && !_connected)
\r
1068 // Remove the envelope corresponding to the messages
\r
1069 var removed = false;
\r
1070 for (var j = 0; j < messageIds.length; ++j)
\r
1072 var id = messageIds[j];
\r
1073 for (var key in _envelopes)
\r
1075 var ids = key.split(',');
\r
1076 var index = org.cometd.Utils.inArray(id, ids);
\r
1080 ids.splice(index, 1);
\r
1081 var envelope = _envelopes[key][0];
\r
1082 var metaConnect = _envelopes[key][1];
\r
1083 delete _envelopes[key];
\r
1084 if (ids.length > 0)
\r
1086 _envelopes[ids.join(',')] = [envelope, metaConnect];
\r
1094 this._debug('Transport', this.getType(), 'removed envelope, envelopes', _envelopes);
\r
1097 _successCallback.call(this, messages);
\r
1101 _webSocket.close(1000, 'Disconnect');
\r
1105 _self.onClose = function(code, reason)
\r
1107 this._debug('Transport', this.getType(), 'closed', code, reason, _webSocket);
\r
1109 // Remember if we were able to connect
\r
1110 // This close event could be due to server shutdown, and if it restarts we want to try websocket again
\r
1111 _supportsWebSocket = _webSocketSupported;
\r
1113 for (var id in _timeouts)
\r
1115 this.clearTimeout(_timeouts[id]);
\r
1119 for (var key in _envelopes)
\r
1121 var envelope = _envelopes[key][0];
\r
1122 var metaConnect = _envelopes[key][1];
\r
1125 _connected = false;
\r
1127 envelope.onFailure(_webSocket, envelope.messages, 'closed ' + code + '/' + reason);
\r
1131 if (_webSocket !== null && _opened)
\r
1133 _webSocket.close(1000, 'Close');
\r
1136 _webSocket = null;
\r
1139 _self.registered = function(type, cometd)
\r
1141 _super.registered(type, cometd);
\r
1145 _self.accept = function(version, crossDomain, url)
\r
1147 // Using !! to return a boolean (and not the WebSocket object)
\r
1148 return _supportsWebSocket && !!org.cometd.WebSocket && _cometd.websocketEnabled !== false;
\r
1151 _self.send = function(envelope, metaConnect)
\r
1153 this._debug('Transport', this.getType(), 'sending', envelope, 'metaConnect =', metaConnect);
\r
1155 // Store the envelope in any case; if the websocket cannot be opened, we fail it in close()
\r
1156 var messageIds = [];
\r
1157 for (var i = 0; i < envelope.messages.length; ++i)
\r
1159 var message = envelope.messages[i];
\r
1162 messageIds.push(message.id);
\r
1165 _envelopes[messageIds.join(',')] = [envelope, metaConnect];
\r
1166 this._debug('Transport', this.getType(), 'stored envelope, envelopes', _envelopes);
\r
1168 _send.call(this, envelope, metaConnect);
\r
1171 _self.abort = function()
\r
1174 if (_webSocket !== null)
\r
1178 _webSocket.close(1001);
\r
1182 // Firefox may throw, just ignore
\r
1189 _self.reset = function()
\r
1192 if (_webSocket !== null && _opened)
\r
1194 _webSocket.close(1000, 'Reset');
\r
1196 _supportsWebSocket = true;
\r
1197 _webSocketSupported = false;
\r
1200 _webSocket = null;
\r
1202 _successCallback = null;
\r
1209 * The constructor for a Cometd object, identified by an optional name.
\r
1210 * The default name is the string 'default'.
\r
1211 * In the rare case a page needs more than one Bayeux conversation,
\r
1212 * a new instance can be created via:
\r
1214 * var bayeuxUrl2 = ...;
\r
1217 * var cometd2 = new dojox.Cometd('another_optional_name');
\r
1220 * var cometd2 = new $.Cometd('another_optional_name');
\r
1222 * cometd2.init({url: bayeuxUrl2});
\r
1224 * @param name the optional name of this cometd object
\r
1226 // IMPLEMENTATION NOTES:
\r
1227 // Be very careful in not changing the function order and pass this file every time through JSLint (http://jslint.com)
\r
1228 // The only implied globals must be "dojo", "org" and "window", and check that there are no "unused" warnings
\r
1229 // Failing to pass JSLint may result in shrinkers/minifiers to create an unusable file.
\r
1230 org.cometd.Cometd = function(name)
\r
1232 var _cometd = this;
\r
1233 var _name = name || 'default';
\r
1234 var _crossDomain = false;
\r
1235 var _transports = new org.cometd.TransportRegistry();
\r
1237 var _status = 'disconnected';
\r
1238 var _messageId = 0;
\r
1239 var _clientId = null;
\r
1241 var _messageQueue = [];
\r
1242 var _internalBatch = false;
\r
1243 var _listeners = {};
\r
1245 var _scheduledSend = null;
\r
1246 var _extensions = [];
\r
1248 var _handshakeProps;
\r
1249 var _publishCallbacks = {};
\r
1250 var _reestablish = false;
\r
1251 var _connected = false;
\r
1253 connectTimeout: 0,
\r
1254 maxConnections: 2,
\r
1255 backoffIncrement: 1000,
\r
1256 maxBackoff: 60000,
\r
1258 reverseIncomingExtensions: true,
\r
1259 maxNetworkDelay: 10000,
\r
1260 requestHeaders: {},
\r
1261 appendMessageTypeToURL: true,
\r
1266 reconnect: 'retry'
\r
1271 * Mixes in the given objects into the target object by copying the properties.
\r
1272 * @param deep if the copy must be deep
\r
1273 * @param target the target object
\r
1274 * @param objects the objects whose properties are copied into the target
\r
1276 this._mixin = function(deep, target, objects)
\r
1278 var result = target || {};
\r
1280 // Skip first 2 parameters (deep and target), and loop over the others
\r
1281 for (var i = 2; i < arguments.length; ++i)
\r
1283 var object = arguments[i];
\r
1285 if (object === undefined || object === null)
\r
1290 for (var propName in object)
\r
1292 var prop = object[propName];
\r
1293 var targ = result[propName];
\r
1295 // Avoid infinite loops
\r
1296 if (prop === target)
\r
1300 // Do not mixin undefined values
\r
1301 if (prop === undefined)
\r
1306 if (deep && typeof prop === 'object' && prop !== null)
\r
1308 if (prop instanceof Array)
\r
1310 result[propName] = this._mixin(deep, targ instanceof Array ? targ : [], prop);
\r
1314 var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {};
\r
1315 result[propName] = this._mixin(deep, source, prop);
\r
1320 result[propName] = prop;
\r
1328 function _isString(value)
\r
1330 return org.cometd.Utils.isString(value);
\r
1333 function _isFunction(value)
\r
1335 if (value === undefined || value === null)
\r
1339 return typeof value === 'function';
\r
1342 function _log(level, args)
\r
1344 if (window.console)
\r
1346 var logger = window.console[level];
\r
1347 if (_isFunction(logger))
\r
1349 logger.apply(window.console, args);
\r
1354 this._warn = function()
\r
1356 _log('warn', arguments);
\r
1359 this._info = function()
\r
1361 if (_config.logLevel !== 'warn')
\r
1363 _log('info', arguments);
\r
1367 this._debug = function()
\r
1369 if (_config.logLevel === 'debug')
\r
1371 _log('debug', arguments);
\r
1376 * Returns whether the given hostAndPort is cross domain.
\r
1377 * The default implementation checks against window.location.host
\r
1378 * but this function can be overridden to make it work in non-browser
\r
1381 * @param hostAndPort the host and port in format host:port
\r
1382 * @return whether the given hostAndPort is cross domain
\r
1384 this._isCrossDomain = function(hostAndPort)
\r
1386 return hostAndPort && hostAndPort !== window.location.host;
\r
1389 function _configure(configuration)
\r
1391 _cometd._debug('Configuring cometd object with', configuration);
\r
1392 // Support old style param, where only the Bayeux server URL was passed
\r
1393 if (_isString(configuration))
\r
1395 configuration = { url: configuration };
\r
1397 if (!configuration)
\r
1399 configuration = {};
\r
1402 _config = _cometd._mixin(false, _config, configuration);
\r
1406 throw 'Missing required configuration parameter \'url\' specifying the Bayeux server URL';
\r
1409 // Check if we're cross domain
\r
1410 // [1] = protocol://, [2] = host:port, [3] = host, [4] = IPv6_host, [5] = IPv4_host, [6] = :port, [7] = port, [8] = uri, [9] = rest
\r
1411 var urlParts = /(^https?:\/\/)?(((\[[^\]]+\])|([^:\/\?#]+))(:(\d+))?)?([^\?#]*)(.*)?/.exec(_config.url);
\r
1412 var hostAndPort = urlParts[2];
\r
1413 var uri = urlParts[8];
\r
1414 var afterURI = urlParts[9];
\r
1415 _crossDomain = _cometd._isCrossDomain(hostAndPort);
\r
1417 // Check if appending extra path is supported
\r
1418 if (_config.appendMessageTypeToURL)
\r
1420 if (afterURI !== undefined && afterURI.length > 0)
\r
1422 _cometd._info('Appending message type to URI ' + uri + afterURI + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
\r
1423 _config.appendMessageTypeToURL = false;
\r
1427 var uriSegments = uri.split('/');
\r
1428 var lastSegmentIndex = uriSegments.length - 1;
\r
1429 if (uri.match(/\/$/))
\r
1431 lastSegmentIndex -= 1;
\r
1433 if (uriSegments[lastSegmentIndex].indexOf('.') >= 0)
\r
1435 // Very likely the CometD servlet's URL pattern is mapped to an extension, such as *.cometd
\r
1436 // It will be difficult to add the extra path in this case
\r
1437 _cometd._info('Appending message type to URI ' + uri + ' is not supported, disabling \'appendMessageTypeToURL\' configuration');
\r
1438 _config.appendMessageTypeToURL = false;
\r
1444 function _clearSubscriptions()
\r
1446 for (var channel in _listeners)
\r
1448 var subscriptions = _listeners[channel];
\r
1449 for (var i = 0; i < subscriptions.length; ++i)
\r
1451 var subscription = subscriptions[i];
\r
1452 if (subscription && !subscription.listener)
\r
1454 delete subscriptions[i];
\r
1455 _cometd._debug('Removed subscription', subscription, 'for channel', channel);
\r
1461 function _setStatus(newStatus)
\r
1463 if (_status !== newStatus)
\r
1465 _cometd._debug('Status', _status, '->', newStatus);
\r
1466 _status = newStatus;
\r
1470 function _isDisconnected()
\r
1472 return _status === 'disconnecting' || _status === 'disconnected';
\r
1475 function _nextMessageId()
\r
1477 return ++_messageId;
\r
1480 function _applyExtension(scope, callback, name, message, outgoing)
\r
1484 return callback.call(scope, message);
\r
1488 _cometd._debug('Exception during execution of extension', name, x);
\r
1489 var exceptionCallback = _cometd.onExtensionException;
\r
1490 if (_isFunction(exceptionCallback))
\r
1492 _cometd._debug('Invoking extension exception callback', name, x);
\r
1495 exceptionCallback.call(_cometd, x, name, outgoing, message);
\r
1499 _cometd._info('Exception during execution of exception callback in extension', name, xx);
\r
1506 function _applyIncomingExtensions(message)
\r
1508 for (var i = 0; i < _extensions.length; ++i)
\r
1510 if (message === undefined || message === null)
\r
1515 var index = _config.reverseIncomingExtensions ? _extensions.length - 1 - i : i;
\r
1516 var extension = _extensions[index];
\r
1517 var callback = extension.extension.incoming;
\r
1518 if (_isFunction(callback))
\r
1520 var result = _applyExtension(extension.extension, callback, extension.name, message, false);
\r
1521 message = result === undefined ? message : result;
\r
1527 function _applyOutgoingExtensions(message)
\r
1529 for (var i = 0; i < _extensions.length; ++i)
\r
1531 if (message === undefined || message === null)
\r
1536 var extension = _extensions[i];
\r
1537 var callback = extension.extension.outgoing;
\r
1538 if (_isFunction(callback))
\r
1540 var result = _applyExtension(extension.extension, callback, extension.name, message, true);
\r
1541 message = result === undefined ? message : result;
\r
1547 function _notify(channel, message)
\r
1549 var subscriptions = _listeners[channel];
\r
1550 if (subscriptions && subscriptions.length > 0)
\r
1552 for (var i = 0; i < subscriptions.length; ++i)
\r
1554 var subscription = subscriptions[i];
\r
1555 // Subscriptions may come and go, so the array may have 'holes'
\r
1560 subscription.callback.call(subscription.scope, message);
\r
1564 _cometd._debug('Exception during notification', subscription, message, x);
\r
1565 var listenerCallback = _cometd.onListenerException;
\r
1566 if (_isFunction(listenerCallback))
\r
1568 _cometd._debug('Invoking listener exception callback', subscription, x);
\r
1571 listenerCallback.call(_cometd, x, subscription.handle, subscription.listener, message);
\r
1575 _cometd._info('Exception during execution of listener callback', subscription, xx);
\r
1584 function _notifyListeners(channel, message)
\r
1586 // Notify direct listeners
\r
1587 _notify(channel, message);
\r
1589 // Notify the globbing listeners
\r
1590 var channelParts = channel.split('/');
\r
1591 var last = channelParts.length - 1;
\r
1592 for (var i = last; i > 0; --i)
\r
1594 var channelPart = channelParts.slice(0, i).join('/') + '/*';
\r
1595 // We don't want to notify /foo/* if the channel is /foo/bar/baz,
\r
1596 // so we stop at the first non recursive globbing
\r
1599 _notify(channelPart, message);
\r
1601 // Add the recursive globber and notify
\r
1602 channelPart += '*';
\r
1603 _notify(channelPart, message);
\r
1607 function _cancelDelayedSend()
\r
1609 if (_scheduledSend !== null)
\r
1611 org.cometd.Utils.clearTimeout(_scheduledSend);
\r
1613 _scheduledSend = null;
\r
1616 function _delayedSend(operation)
\r
1618 _cancelDelayedSend();
\r
1619 var delay = _advice.interval + _backoff;
\r
1620 _cometd._debug('Function scheduled in', delay, 'ms, interval =', _advice.interval, 'backoff =', _backoff, operation);
\r
1621 _scheduledSend = org.cometd.Utils.setTimeout(_cometd, operation, delay);
\r
1624 // Needed to break cyclic dependencies between function definitions
\r
1625 var _handleMessages;
\r
1626 var _handleFailure;
\r
1629 * Delivers the messages to the CometD server
\r
1630 * @param messages the array of messages to send
\r
1631 * @param longpoll true if this send is a long poll
\r
1633 function _send(sync, messages, longpoll, extraPath)
\r
1635 // We must be sure that the messages have a clientId.
\r
1636 // This is not guaranteed since the handshake may take time to return
\r
1637 // (and hence the clientId is not known yet) and the application
\r
1638 // may create other messages.
\r
1639 for (var i = 0; i < messages.length; ++i)
\r
1641 var message = messages[i];
\r
1642 message.id = '' + _nextMessageId();
\r
1646 message.clientId = _clientId;
\r
1649 var callback = undefined;
\r
1650 if (_isFunction(message._callback))
\r
1652 callback = message._callback;
\r
1653 // Remove the publish callback before calling the extensions
\r
1654 delete message._callback;
\r
1657 message = _applyOutgoingExtensions(message);
\r
1658 if (message !== undefined && message !== null)
\r
1660 messages[i] = message;
\r
1662 _publishCallbacks[message.id] = callback;
\r
1666 messages.splice(i--, 1);
\r
1670 if (messages.length === 0)
\r
1675 var url = _config.url;
\r
1676 if (_config.appendMessageTypeToURL)
\r
1678 // If url does not end with '/', then append it
\r
1679 if (!url.match(/\/$/))
\r
1685 url = url + extraPath;
\r
1692 messages: messages,
\r
1693 onSuccess: function(rcvdMessages)
\r
1697 _handleMessages.call(_cometd, rcvdMessages);
\r
1701 _cometd._debug('Exception during handling of messages', x);
\r
1704 onFailure: function(conduit, messages, reason, exception)
\r
1708 _handleFailure.call(_cometd, conduit, messages, reason, exception);
\r
1712 _cometd._debug('Exception during handling of failure', x);
\r
1716 _cometd._debug('Send', envelope);
\r
1717 _transport.send(envelope, longpoll);
\r
1720 function _queueSend(message)
\r
1722 if (_batch > 0 || _internalBatch === true)
\r
1724 _messageQueue.push(message);
\r
1728 _send(false, [message], false);
\r
1733 * Sends a complete bayeux message.
\r
1734 * This method is exposed as a public so that extensions may use it
\r
1735 * to send bayeux message directly, for example in case of re-sending
\r
1736 * messages that have already been sent but that for some reason must
\r
1739 this.send = _queueSend;
\r
1741 function _resetBackoff()
\r
1746 function _increaseBackoff()
\r
1748 if (_backoff < _config.maxBackoff)
\r
1750 _backoff += _config.backoffIncrement;
\r
1755 * Starts a the batch of messages to be sent in a single request.
\r
1756 * @see #_endBatch(sendMessages)
\r
1758 function _startBatch()
\r
1763 function _flushBatch()
\r
1765 var messages = _messageQueue;
\r
1766 _messageQueue = [];
\r
1767 if (messages.length > 0)
\r
1769 _send(false, messages, false);
\r
1774 * Ends the batch of messages to be sent in a single request,
\r
1775 * optionally sending messages present in the message queue depending
\r
1776 * on the given argument.
\r
1777 * @see #_startBatch()
\r
1779 function _endBatch()
\r
1784 throw 'Calls to startBatch() and endBatch() are not paired';
\r
1787 if (_batch === 0 && !_isDisconnected() && !_internalBatch)
\r
1794 * Sends the connect message
\r
1796 function _connect()
\r
1798 if (!_isDisconnected())
\r
1801 channel: '/meta/connect',
\r
1802 connectionType: _transport.getType()
\r
1805 // In case of reload or temporary loss of connection
\r
1806 // we want the next successful connect to return immediately
\r
1807 // instead of being held by the server, so that connect listeners
\r
1808 // can be notified that the connection has been re-established
\r
1811 message.advice = { timeout: 0 };
\r
1814 _setStatus('connecting');
\r
1815 _cometd._debug('Connect sent', message);
\r
1816 _send(false, [message], true, 'connect');
\r
1817 _setStatus('connected');
\r
1821 function _delayedConnect()
\r
1823 _setStatus('connecting');
\r
1824 _delayedSend(function()
\r
1830 function _updateAdvice(newAdvice)
\r
1834 _advice = _cometd._mixin(false, {}, _config.advice, newAdvice);
\r
1835 _cometd._debug('New advice', _advice);
\r
1839 function _disconnect(abort)
\r
1841 _cancelDelayedSend();
\r
1844 _transport.abort();
\r
1847 _setStatus('disconnected');
\r
1851 // Fail any existing queued message
\r
1852 if (_messageQueue.length > 0)
\r
1854 _handleFailure.call(_cometd, undefined, _messageQueue, 'error', 'Disconnected');
\r
1855 _messageQueue = [];
\r
1860 * Sends the initial handshake message
\r
1862 function _handshake(handshakeProps)
\r
1866 _clearSubscriptions();
\r
1868 // Reset the transports if we're not retrying the handshake
\r
1869 if (_isDisconnected())
\r
1871 _transports.reset();
\r
1872 _updateAdvice(_config.advice);
\r
1876 // We are retrying the handshake, either because another handshake failed
\r
1877 // and we're backing off, or because the server timed us out and asks us to
\r
1878 // re-handshake: in both cases, make sure that if the handshake succeeds
\r
1879 // the next action is a connect.
\r
1880 _updateAdvice(_cometd._mixin(false, _advice, {reconnect: 'retry'}));
\r
1885 // Mark the start of an internal batch.
\r
1886 // This is needed because handshake and connect are async.
\r
1887 // It may happen that the application calls init() then subscribe()
\r
1888 // and the subscribe message is sent before the connect message, if
\r
1889 // the subscribe message is not held until the connect message is sent.
\r
1890 // So here we start a batch to hold temporarily any message until
\r
1891 // the connection is fully established.
\r
1892 _internalBatch = true;
\r
1894 // Save the properties provided by the user, so that
\r
1895 // we can reuse them during automatic re-handshake
\r
1896 _handshakeProps = handshakeProps;
\r
1898 var version = '1.0';
\r
1900 // Figure out the transports to send to the server
\r
1901 var transportTypes = _transports.findTransportTypes(version, _crossDomain, _config.url);
\r
1903 var bayeuxMessage = {
\r
1905 minimumVersion: '0.9',
\r
1906 channel: '/meta/handshake',
\r
1907 supportedConnectionTypes: transportTypes,
\r
1909 timeout: _advice.timeout,
\r
1910 interval: _advice.interval
\r
1913 // Do not allow the user to mess with the required properties,
\r
1914 // so merge first the user properties and *then* the bayeux message
\r
1915 var message = _cometd._mixin(false, {}, _handshakeProps, bayeuxMessage);
\r
1917 // Pick up the first available transport as initial transport
\r
1918 // since we don't know if the server supports it
\r
1919 _transport = _transports.negotiateTransport(transportTypes, version, _crossDomain, _config.url);
\r
1920 _cometd._debug('Initial transport is', _transport.getType());
\r
1922 // We started a batch to hold the application messages,
\r
1923 // so here we must bypass it and send immediately.
\r
1924 _setStatus('handshaking');
\r
1925 _cometd._debug('Handshake sent', message);
\r
1926 _send(false, [message], false, 'handshake');
\r
1929 function _delayedHandshake()
\r
1931 _setStatus('handshaking');
\r
1933 // We will call _handshake() which will reset _clientId, but we want to avoid
\r
1934 // that between the end of this method and the call to _handshake() someone may
\r
1935 // call publish() (or other methods that call _queueSend()).
\r
1936 _internalBatch = true;
\r
1938 _delayedSend(function()
\r
1940 _handshake(_handshakeProps);
\r
1944 function _failHandshake(message)
\r
1946 _notifyListeners('/meta/handshake', message);
\r
1947 _notifyListeners('/meta/unsuccessful', message);
\r
1949 // Only try again if we haven't been disconnected and
\r
1950 // the advice permits us to retry the handshake
\r
1951 var retry = !_isDisconnected() && _advice.reconnect !== 'none';
\r
1954 _increaseBackoff();
\r
1955 _delayedHandshake();
\r
1959 _disconnect(false);
\r
1963 function _handshakeResponse(message)
\r
1965 if (message.successful)
\r
1967 // Save clientId, figure out transport, then follow the advice to connect
\r
1968 _clientId = message.clientId;
\r
1970 var newTransport = _transports.negotiateTransport(message.supportedConnectionTypes, message.version, _crossDomain, _config.url);
\r
1971 if (newTransport === null)
\r
1973 throw 'Could not negotiate transport with server; client ' +
\r
1974 _transports.findTransportTypes(message.version, _crossDomain, _config.url) +
\r
1975 ', server ' + message.supportedConnectionTypes;
\r
1977 else if (_transport !== newTransport)
\r
1979 _cometd._debug('Transport', _transport, '->', newTransport);
\r
1980 _transport = newTransport;
\r
1983 // End the internal batch and allow held messages from the application
\r
1984 // to go to the server (see _handshake() where we start the internal batch).
\r
1985 _internalBatch = false;
\r
1988 // Here the new transport is in place, as well as the clientId, so
\r
1989 // the listeners can perform a publish() if they want.
\r
1990 // Notify the listeners before the connect below.
\r
1991 message.reestablish = _reestablish;
\r
1992 _reestablish = true;
\r
1993 _notifyListeners('/meta/handshake', message);
\r
1995 var action = _isDisconnected() ? 'none' : _advice.reconnect;
\r
2000 _delayedConnect();
\r
2003 _disconnect(false);
\r
2006 throw 'Unrecognized advice action ' + action;
\r
2011 _failHandshake(message);
\r
2015 function _handshakeFailure(xhr, message)
\r
2018 successful: false,
\r
2020 channel: '/meta/handshake',
\r
2024 reconnect: 'retry',
\r
2025 interval: _backoff
\r
2030 function _failConnect(message)
\r
2032 // Notify the listeners after the status change but before the next action
\r
2033 _notifyListeners('/meta/connect', message);
\r
2034 _notifyListeners('/meta/unsuccessful', message);
\r
2036 // This may happen when the server crashed, the current clientId
\r
2037 // will be invalid, and the server will ask to handshake again
\r
2038 // Listeners can call disconnect(), so check the state after they run
\r
2039 var action = _isDisconnected() ? 'none' : _advice.reconnect;
\r
2043 _delayedConnect();
\r
2044 _increaseBackoff();
\r
2047 // The current transport may be failed (e.g. network disconnection)
\r
2048 // Reset the transports so the new handshake picks up the right one
\r
2049 _transports.reset();
\r
2051 _delayedHandshake();
\r
2054 _disconnect(false);
\r
2057 throw 'Unrecognized advice action' + action;
\r
2061 function _connectResponse(message)
\r
2063 _connected = message.successful;
\r
2067 _notifyListeners('/meta/connect', message);
\r
2069 // Normally, the advice will say "reconnect: 'retry', interval: 0"
\r
2070 // and the server will hold the request, so when a response returns
\r
2071 // we immediately call the server again (long polling)
\r
2072 // Listeners can call disconnect(), so check the state after they run
\r
2073 var action = _isDisconnected() ? 'none' : _advice.reconnect;
\r
2078 _delayedConnect();
\r
2081 _disconnect(false);
\r
2084 throw 'Unrecognized advice action ' + action;
\r
2089 _failConnect(message);
\r
2093 function _connectFailure(xhr, message)
\r
2095 _connected = false;
\r
2097 successful: false,
\r
2099 channel: '/meta/connect',
\r
2103 reconnect: 'retry',
\r
2104 interval: _backoff
\r
2109 function _failDisconnect(message)
\r
2111 _disconnect(true);
\r
2112 _notifyListeners('/meta/disconnect', message);
\r
2113 _notifyListeners('/meta/unsuccessful', message);
\r
2116 function _disconnectResponse(message)
\r
2118 if (message.successful)
\r
2120 _disconnect(false);
\r
2121 _notifyListeners('/meta/disconnect', message);
\r
2125 _failDisconnect(message);
\r
2129 function _disconnectFailure(xhr, message)
\r
2132 successful: false,
\r
2134 channel: '/meta/disconnect',
\r
2138 reconnect: 'none',
\r
2144 function _failSubscribe(message)
\r
2146 _notifyListeners('/meta/subscribe', message);
\r
2147 _notifyListeners('/meta/unsuccessful', message);
\r
2150 function _subscribeResponse(message)
\r
2152 if (message.successful)
\r
2154 _notifyListeners('/meta/subscribe', message);
\r
2158 _failSubscribe(message);
\r
2162 function _subscribeFailure(xhr, message)
\r
2165 successful: false,
\r
2167 channel: '/meta/subscribe',
\r
2171 reconnect: 'none',
\r
2177 function _failUnsubscribe(message)
\r
2179 _notifyListeners('/meta/unsubscribe', message);
\r
2180 _notifyListeners('/meta/unsuccessful', message);
\r
2183 function _unsubscribeResponse(message)
\r
2185 if (message.successful)
\r
2187 _notifyListeners('/meta/unsubscribe', message);
\r
2191 _failUnsubscribe(message);
\r
2195 function _unsubscribeFailure(xhr, message)
\r
2197 _failUnsubscribe({
\r
2198 successful: false,
\r
2200 channel: '/meta/unsubscribe',
\r
2204 reconnect: 'none',
\r
2210 function _handlePublishCallback(message)
\r
2212 var callback = _publishCallbacks[message.id];
\r
2213 if (_isFunction(callback))
\r
2215 delete _publishCallbacks[message.id];
\r
2216 callback.call(_cometd, message);
\r
2220 function _failMessage(message)
\r
2222 _handlePublishCallback(message);
\r
2223 _notifyListeners('/meta/publish', message);
\r
2224 _notifyListeners('/meta/unsuccessful', message);
\r
2227 function _messageResponse(message)
\r
2229 if (message.successful === undefined)
\r
2233 // It is a plain message, and not a bayeux meta message
\r
2234 _notifyListeners(message.channel, message);
\r
2238 _cometd._debug('Unknown message', message);
\r
2243 if (message.successful)
\r
2245 _handlePublishCallback(message);
\r
2246 _notifyListeners('/meta/publish', message);
\r
2250 _failMessage(message);
\r
2255 function _messageFailure(xhr, message)
\r
2258 successful: false,
\r
2260 channel: message.channel,
\r
2264 reconnect: 'none',
\r
2270 function _receive(message)
\r
2272 message = _applyIncomingExtensions(message);
\r
2273 if (message === undefined || message === null)
\r
2278 _updateAdvice(message.advice);
\r
2280 var channel = message.channel;
\r
2283 case '/meta/handshake':
\r
2284 _handshakeResponse(message);
\r
2286 case '/meta/connect':
\r
2287 _connectResponse(message);
\r
2289 case '/meta/disconnect':
\r
2290 _disconnectResponse(message);
\r
2292 case '/meta/subscribe':
\r
2293 _subscribeResponse(message);
\r
2295 case '/meta/unsubscribe':
\r
2296 _unsubscribeResponse(message);
\r
2299 _messageResponse(message);
\r
2305 * Receives a message.
\r
2306 * This method is exposed as a public so that extensions may inject
\r
2307 * messages simulating that they had been received.
\r
2309 this.receive = _receive;
\r
2311 _handleMessages = function(rcvdMessages)
\r
2313 _cometd._debug('Received', rcvdMessages);
\r
2315 for (var i = 0; i < rcvdMessages.length; ++i)
\r
2317 var message = rcvdMessages[i];
\r
2318 _receive(message);
\r
2322 _handleFailure = function(conduit, messages, reason, exception)
\r
2324 _cometd._debug('handleFailure', conduit, messages, reason, exception);
\r
2326 for (var i = 0; i < messages.length; ++i)
\r
2328 var message = messages[i];
\r
2329 var channel = message.channel;
\r
2332 case '/meta/handshake':
\r
2333 _handshakeFailure(conduit, message);
\r
2335 case '/meta/connect':
\r
2336 _connectFailure(conduit, message);
\r
2338 case '/meta/disconnect':
\r
2339 _disconnectFailure(conduit, message);
\r
2341 case '/meta/subscribe':
\r
2342 _subscribeFailure(conduit, message);
\r
2344 case '/meta/unsubscribe':
\r
2345 _unsubscribeFailure(conduit, message);
\r
2348 _messageFailure(conduit, message);
\r
2354 function _hasSubscriptions(channel)
\r
2356 var subscriptions = _listeners[channel];
\r
2357 if (subscriptions)
\r
2359 for (var i = 0; i < subscriptions.length; ++i)
\r
2361 if (subscriptions[i])
\r
2370 function _resolveScopedCallback(scope, callback)
\r
2376 if (_isFunction(scope))
\r
2378 delegate.scope = undefined;
\r
2379 delegate.method = scope;
\r
2383 if (_isString(callback))
\r
2387 throw 'Invalid scope ' + scope;
\r
2389 delegate.method = scope[callback];
\r
2390 if (!_isFunction(delegate.method))
\r
2392 throw 'Invalid callback ' + callback + ' for scope ' + scope;
\r
2395 else if (!_isFunction(callback))
\r
2397 throw 'Invalid callback ' + callback;
\r
2403 function _addListener(channel, scope, callback, isListener)
\r
2405 // The data structure is a map<channel, subscription[]>, where each subscription
\r
2406 // holds the callback to be called and its scope.
\r
2408 var delegate = _resolveScopedCallback(scope, callback);
\r
2409 _cometd._debug('Adding listener on', channel, 'with scope', delegate.scope, 'and callback', delegate.method);
\r
2411 var subscription = {
\r
2413 scope: delegate.scope,
\r
2414 callback: delegate.method,
\r
2415 listener: isListener
\r
2418 var subscriptions = _listeners[channel];
\r
2419 if (!subscriptions)
\r
2421 subscriptions = [];
\r
2422 _listeners[channel] = subscriptions;
\r
2425 // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
\r
2427 // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
\r
2429 // hc==3, a.join()=='a',,'c', a.length==3
\r
2430 var subscriptionID = subscriptions.push(subscription) - 1;
\r
2431 subscription.id = subscriptionID;
\r
2432 subscription.handle = [channel, subscriptionID];
\r
2434 _cometd._debug('Added listener', subscription, 'for channel', channel, 'having id =', subscriptionID);
\r
2436 // The subscription to allow removal of the listener is made of the channel and the index
\r
2437 return subscription.handle;
\r
2440 function _removeListener(subscription)
\r
2442 var subscriptions = _listeners[subscription[0]];
\r
2443 if (subscriptions)
\r
2445 delete subscriptions[subscription[1]];
\r
2446 _cometd._debug('Removed listener', subscription);
\r
2455 * Registers the given transport under the given transport type.
\r
2456 * The optional index parameter specifies the "priority" at which the
\r
2457 * transport is registered (where 0 is the max priority).
\r
2458 * If a transport with the same type is already registered, this function
\r
2459 * does nothing and returns false.
\r
2460 * @param type the transport type
\r
2461 * @param transport the transport object
\r
2462 * @param index the index at which this transport is to be registered
\r
2463 * @return true if the transport has been registered, false otherwise
\r
2464 * @see #unregisterTransport(type)
\r
2466 this.registerTransport = function(type, transport, index)
\r
2468 var result = _transports.add(type, transport, index);
\r
2471 this._debug('Registered transport', type);
\r
2473 if (_isFunction(transport.registered))
\r
2475 transport.registered(type, this);
\r
2482 * @return an array of all registered transport types
\r
2484 this.getTransportTypes = function()
\r
2486 return _transports.getTransportTypes();
\r
2490 * Unregisters the transport with the given transport type.
\r
2491 * @param type the transport type to unregister
\r
2492 * @return the transport that has been unregistered,
\r
2493 * or null if no transport was previously registered under the given transport type
\r
2495 this.unregisterTransport = function(type)
\r
2497 var transport = _transports.remove(type);
\r
2498 if (transport !== null)
\r
2500 this._debug('Unregistered transport', type);
\r
2502 if (_isFunction(transport.unregistered))
\r
2504 transport.unregistered();
\r
2510 this.unregisterTransports = function()
\r
2512 _transports.clear();
\r
2515 this.findTransport = function(name)
\r
2517 return _transports.find(name);
\r
2521 * Configures the initial Bayeux communication with the Bayeux server.
\r
2522 * Configuration is passed via an object that must contain a mandatory field <code>url</code>
\r
2523 * of type string containing the URL of the Bayeux server.
\r
2524 * @param configuration the configuration object
\r
2526 this.configure = function(configuration)
\r
2528 _configure.call(this, configuration);
\r
2532 * Configures and establishes the Bayeux communication with the Bayeux server
\r
2533 * via a handshake and a subsequent connect.
\r
2534 * @param configuration the configuration object
\r
2535 * @param handshakeProps an object to be merged with the handshake message
\r
2536 * @see #configure(configuration)
\r
2537 * @see #handshake(handshakeProps)
\r
2539 this.init = function(configuration, handshakeProps)
\r
2541 this.configure(configuration);
\r
2542 this.handshake(handshakeProps);
\r
2546 * Establishes the Bayeux communication with the Bayeux server
\r
2547 * via a handshake and a subsequent connect.
\r
2548 * @param handshakeProps an object to be merged with the handshake message
\r
2550 this.handshake = function(handshakeProps)
\r
2552 _setStatus('disconnected');
\r
2553 _reestablish = false;
\r
2554 _handshake(handshakeProps);
\r
2558 * Disconnects from the Bayeux server.
\r
2559 * It is possible to suggest to attempt a synchronous disconnect, but this feature
\r
2560 * may only be available in certain transports (for example, long-polling may support
\r
2561 * it, callback-polling certainly does not).
\r
2562 * @param sync whether attempt to perform a synchronous disconnect
\r
2563 * @param disconnectProps an object to be merged with the disconnect message
\r
2565 this.disconnect = function(sync, disconnectProps)
\r
2567 if (_isDisconnected())
\r
2572 if (disconnectProps === undefined)
\r
2574 if (typeof sync !== 'boolean')
\r
2576 disconnectProps = sync;
\r
2581 var bayeuxMessage = {
\r
2582 channel: '/meta/disconnect'
\r
2584 var message = this._mixin(false, {}, disconnectProps, bayeuxMessage);
\r
2585 _setStatus('disconnecting');
\r
2586 _send(sync === true, [message], false, 'disconnect');
\r
2590 * Marks the start of a batch of application messages to be sent to the server
\r
2591 * in a single request, obtaining a single response containing (possibly) many
\r
2592 * application reply messages.
\r
2593 * Messages are held in a queue and not sent until {@link #endBatch()} is called.
\r
2594 * If startBatch() is called multiple times, then an equal number of endBatch()
\r
2595 * calls must be made to close and send the batch of messages.
\r
2596 * @see #endBatch()
\r
2598 this.startBatch = function()
\r
2604 * Marks the end of a batch of application messages to be sent to the server
\r
2605 * in a single request.
\r
2606 * @see #startBatch()
\r
2608 this.endBatch = function()
\r
2614 * Executes the given callback in the given scope, surrounded by a {@link #startBatch()}
\r
2615 * and {@link #endBatch()} calls.
\r
2616 * @param scope the scope of the callback, may be omitted
\r
2617 * @param callback the callback to be executed within {@link #startBatch()} and {@link #endBatch()} calls
\r
2619 this.batch = function(scope, callback)
\r
2621 var delegate = _resolveScopedCallback(scope, callback);
\r
2622 this.startBatch();
\r
2625 delegate.method.call(delegate.scope);
\r
2630 this._debug('Exception during execution of batch', x);
\r
2637 * Adds a listener for bayeux messages, performing the given callback in the given scope
\r
2638 * when a message for the given channel arrives.
\r
2639 * @param channel the channel the listener is interested to
\r
2640 * @param scope the scope of the callback, may be omitted
\r
2641 * @param callback the callback to call when a message is sent to the channel
\r
2642 * @returns the subscription handle to be passed to {@link #removeListener(object)}
\r
2643 * @see #removeListener(subscription)
\r
2645 this.addListener = function(channel, scope, callback)
\r
2647 if (arguments.length < 2)
\r
2649 throw 'Illegal arguments number: required 2, got ' + arguments.length;
\r
2651 if (!_isString(channel))
\r
2653 throw 'Illegal argument type: channel must be a string';
\r
2656 return _addListener(channel, scope, callback, true);
\r
2660 * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
\r
2661 * @param subscription the subscription to unsubscribe.
\r
2662 * @see #addListener(channel, scope, callback)
\r
2664 this.removeListener = function(subscription)
\r
2666 if (!org.cometd.Utils.isArray(subscription))
\r
2668 throw 'Invalid argument: expected subscription, not ' + subscription;
\r
2671 _removeListener(subscription);
\r
2675 * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
\r
2676 * {@link #subscribe(channel, scope, callback)}.
\r
2678 this.clearListeners = function()
\r
2684 * Subscribes to the given channel, performing the given callback in the given scope
\r
2685 * when a message for the channel arrives.
\r
2686 * @param channel the channel to subscribe to
\r
2687 * @param scope the scope of the callback, may be omitted
\r
2688 * @param callback the callback to call when a message is sent to the channel
\r
2689 * @param subscribeProps an object to be merged with the subscribe message
\r
2690 * @return the subscription handle to be passed to {@link #unsubscribe(object)}
\r
2692 this.subscribe = function(channel, scope, callback, subscribeProps)
\r
2694 if (arguments.length < 2)
\r
2696 throw 'Illegal arguments number: required 2, got ' + arguments.length;
\r
2698 if (!_isString(channel))
\r
2700 throw 'Illegal argument type: channel must be a string';
\r
2702 if (_isDisconnected())
\r
2704 throw 'Illegal state: already disconnected';
\r
2707 // Normalize arguments
\r
2708 if (_isFunction(scope))
\r
2710 subscribeProps = callback;
\r
2712 scope = undefined;
\r
2715 // Only send the message to the server if this client has not yet subscribed to the channel
\r
2716 var send = !_hasSubscriptions(channel);
\r
2718 var subscription = _addListener(channel, scope, callback, false);
\r
2722 // Send the subscription message after the subscription registration to avoid
\r
2723 // races where the server would send a message to the subscribers, but here
\r
2724 // on the client the subscription has not been added yet to the data structures
\r
2725 var bayeuxMessage = {
\r
2726 channel: '/meta/subscribe',
\r
2727 subscription: channel
\r
2729 var message = this._mixin(false, {}, subscribeProps, bayeuxMessage);
\r
2730 _queueSend(message);
\r
2733 return subscription;
\r
2737 * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
\r
2738 * @param subscription the subscription to unsubscribe.
\r
2740 this.unsubscribe = function(subscription, unsubscribeProps)
\r
2742 if (arguments.length < 1)
\r
2744 throw 'Illegal arguments number: required 1, got ' + arguments.length;
\r
2746 if (_isDisconnected())
\r
2748 throw 'Illegal state: already disconnected';
\r
2751 // Remove the local listener before sending the message
\r
2752 // This ensures that if the server fails, this client does not get notifications
\r
2753 this.removeListener(subscription);
\r
2755 var channel = subscription[0];
\r
2756 // Only send the message to the server if this client unsubscribes the last subscription
\r
2757 if (!_hasSubscriptions(channel))
\r
2759 var bayeuxMessage = {
\r
2760 channel: '/meta/unsubscribe',
\r
2761 subscription: channel
\r
2763 var message = this._mixin(false, {}, unsubscribeProps, bayeuxMessage);
\r
2764 _queueSend(message);
\r
2769 * Removes all subscriptions added via {@link #subscribe(channel, scope, callback, subscribeProps)},
\r
2770 * but does not remove the listeners added via {@link addListener(channel, scope, callback)}.
\r
2772 this.clearSubscriptions = function()
\r
2774 _clearSubscriptions();
\r
2778 * Publishes a message on the given channel, containing the given content.
\r
2779 * @param channel the channel to publish the message to
\r
2780 * @param content the content of the message
\r
2781 * @param publishProps an object to be merged with the publish message
\r
2783 this.publish = function(channel, content, publishProps, publishCallback)
\r
2785 if (arguments.length < 1)
\r
2787 throw 'Illegal arguments number: required 1, got ' + arguments.length;
\r
2789 if (!_isString(channel))
\r
2791 throw 'Illegal argument type: channel must be a string';
\r
2793 if (_isDisconnected())
\r
2795 throw 'Illegal state: already disconnected';
\r
2798 if (_isFunction(content))
\r
2800 publishCallback = content;
\r
2801 content = publishProps = {};
\r
2803 else if (_isFunction(publishProps))
\r
2805 publishCallback = publishProps;
\r
2806 publishProps = {};
\r
2809 var bayeuxMessage = {
\r
2812 _callback: publishCallback
\r
2814 var message = this._mixin(false, {}, publishProps, bayeuxMessage);
\r
2815 _queueSend(message);
\r
2819 * Returns a string representing the status of the bayeux communication with the Bayeux server.
\r
2821 this.getStatus = function()
\r
2827 * Returns whether this instance has been disconnected.
\r
2829 this.isDisconnected = _isDisconnected;
\r
2832 * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
\r
2833 * Default value is 1 second, which means if there is a persistent failure the retries will happen
\r
2834 * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
\r
2835 * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
\r
2836 * @param period the backoff period to set
\r
2837 * @see #getBackoffIncrement()
\r
2839 this.setBackoffIncrement = function(period)
\r
2841 _config.backoffIncrement = period;
\r
2845 * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
\r
2846 * @see #setBackoffIncrement(period)
\r
2848 this.getBackoffIncrement = function()
\r
2850 return _config.backoffIncrement;
\r
2854 * Returns the backoff period to wait before retrying an unsuccessful or failed message.
\r
2856 this.getBackoffPeriod = function()
\r
2862 * Sets the log level for console logging.
\r
2863 * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
\r
2864 * less verbose to more verbose.
\r
2865 * @param level the log level string
\r
2867 this.setLogLevel = function(level)
\r
2869 _config.logLevel = level;
\r
2873 * Registers an extension whose callbacks are called for every incoming message
\r
2874 * (that comes from the server to this client implementation) and for every
\r
2875 * outgoing message (that originates from this client implementation for the
\r
2877 * The format of the extension object is the following:
\r
2880 * incoming: function(message) { ... },
\r
2881 * outgoing: function(message) { ... }
\r
2884 * Both properties are optional, but if they are present they will be called
\r
2885 * respectively for each incoming message and for each outgoing message.
\r
2886 * @param name the name of the extension
\r
2887 * @param extension the extension to register
\r
2888 * @return true if the extension was registered, false otherwise
\r
2889 * @see #unregisterExtension(name)
\r
2891 this.registerExtension = function(name, extension)
\r
2893 if (arguments.length < 2)
\r
2895 throw 'Illegal arguments number: required 2, got ' + arguments.length;
\r
2897 if (!_isString(name))
\r
2899 throw 'Illegal argument type: extension name must be a string';
\r
2902 var existing = false;
\r
2903 for (var i = 0; i < _extensions.length; ++i)
\r
2905 var existingExtension = _extensions[i];
\r
2906 if (existingExtension.name === name)
\r
2914 _extensions.push({
\r
2916 extension: extension
\r
2918 this._debug('Registered extension', name);
\r
2920 // Callback for extensions
\r
2921 if (_isFunction(extension.registered))
\r
2923 extension.registered(name, this);
\r
2930 this._info('Could not register extension with name', name, 'since another extension with the same name already exists');
\r
2936 * Unregister an extension previously registered with
\r
2937 * {@link #registerExtension(name, extension)}.
\r
2938 * @param name the name of the extension to unregister.
\r
2939 * @return true if the extension was unregistered, false otherwise
\r
2941 this.unregisterExtension = function(name)
\r
2943 if (!_isString(name))
\r
2945 throw 'Illegal argument type: extension name must be a string';
\r
2948 var unregistered = false;
\r
2949 for (var i = 0; i < _extensions.length; ++i)
\r
2951 var extension = _extensions[i];
\r
2952 if (extension.name === name)
\r
2954 _extensions.splice(i, 1);
\r
2955 unregistered = true;
\r
2956 this._debug('Unregistered extension', name);
\r
2958 // Callback for extensions
\r
2959 var ext = extension.extension;
\r
2960 if (_isFunction(ext.unregistered))
\r
2962 ext.unregistered();
\r
2968 return unregistered;
\r
2972 * Find the extension registered with the given name.
\r
2973 * @param name the name of the extension to find
\r
2974 * @return the extension found or null if no extension with the given name has been registered
\r
2976 this.getExtension = function(name)
\r
2978 for (var i = 0; i < _extensions.length; ++i)
\r
2980 var extension = _extensions[i];
\r
2981 if (extension.name === name)
\r
2983 return extension.extension;
\r
2990 * Returns the name assigned to this Cometd object, or the string 'default'
\r
2991 * if no name has been explicitly passed as parameter to the constructor.
\r
2993 this.getName = function()
\r
2999 * Returns the clientId assigned by the Bayeux server during handshake.
\r
3001 this.getClientId = function()
\r
3007 * Returns the URL of the Bayeux server.
\r
3009 this.getURL = function()
\r
3011 return _config.url;
\r
3014 this.getTransport = function()
\r
3016 return _transport;
\r
3019 this.getConfiguration = function()
\r
3021 return this._mixin(true, {}, _config);
\r
3024 this.getAdvice = function()
\r
3026 return this._mixin(true, {}, _advice);
\r
3029 // WebSocket handling for Firefox, which deploys WebSocket
\r
3030 // under the name of MozWebSocket in Firefox 6, 7, 8 and 9
\r
3031 org.cometd.WebSocket = window.WebSocket;
\r
3032 if (!org.cometd.WebSocket)
\r
3034 org.cometd.WebSocket = window.MozWebSocket;
\r
3038 if (typeof define === 'function' && define.amd)
\r
3042 return org.cometd;
\r