2 * Copyright 2008 Mort Bay Consulting Pty. Ltd.
3 * Dual licensed under the Apache License 2.0 and the MIT license.
4 * ----------------------------------------------------------------------------
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 * http: *www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 * ----------------------------------------------------------------------------
15 * Licensed under the MIT license;
16 * Permission is hereby granted, free of charge, to any person obtaining
17 * a copy of this software and associated documentation files (the
18 * "Software"), to deal in the Software without restriction, including
19 * without limitation the rights to use, copy, modify, merge, publish,
20 * distribute, sublicense, and/or sell copies of the Software, and to
21 * permit persons to whom the Software is furnished to do so, subject to
22 * the following conditions:
24 * The above copyright notice and this permission notice shall be
25 * included in all copies or substantial portions of the Software.
27 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
29 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 * ----------------------------------------------------------------------------
40 * The constructor for a Comet object.
41 * There is a default Comet instance already created at the variable <code>$.cometd</code>,
42 * and hence that can be used to start a comet conversation with a server.
43 * In the rare case a page needs more than one comet conversation, a new instance can be
47 * var cometd2 = new $.Cometd();
51 $.Cometd = function(name)
53 var _name = name || 'default';
54 var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 };
55 var _logLevel = 'info';
59 var _status = 'disconnected';
63 var _messageQueue = [];
66 var _backoffIncrement = 1000;
67 var _maxBackoff = 60000;
68 var _scheduledSend = null;
74 * Returns the name assigned to this Comet object, or the string 'default'
75 * if no name has been explicitely passed as parameter to the constructor.
77 this.getName = function()
83 * Configures the initial comet communication with the comet server.
84 * @param cometURL the URL of the comet server
86 this.configure = function(cometURL)
91 function _configure(cometURL)
94 _debug('Initializing comet with url: {}', _url);
96 // Check immediately if we're cross domain
97 // If cross domain, the handshake must not send the long polling transport type
98 var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL);
99 if (urlParts[3]) _xd = urlParts[3] != location.host;
101 // Temporary setup a transport to send the initial handshake
102 // The transport may be changed as a result of handshake
104 _transport = newCallbackPollingTransport();
106 _transport = newLongPollingTransport();
107 _debug('Initial transport is {}', _transport.getType());
111 * Configures and establishes the comet communication with the comet server
112 * via a handshake and a subsequent connect.
113 * @param cometURL the URL of the comet server
114 * @param handshakeProps an object to be merged with the handshake message
115 * @see #configure(cometURL)
116 * @see #handshake(handshakeProps)
118 this.init = function(cometURL, handshakeProps)
120 _configure(cometURL);
121 _handshake(handshakeProps);
125 * Establishes the comet communication with the comet server
126 * via a handshake and a subsequent connect.
127 * @param handshakeProps an object to be merged with the handshake message
129 this.handshake = function(handshakeProps)
131 _handshake(handshakeProps);
135 * Disconnects from the comet server.
136 * @param disconnectProps an object to be merged with the disconnect message
138 this.disconnect = function(disconnectProps)
140 var bayeuxMessage = {
141 channel: '/meta/disconnect'
143 var message = $.extend({}, disconnectProps, bayeuxMessage);
144 // Deliver immediately
145 // The handshake and connect mechanism make use of startBatch(), and in case
146 // of a failed handshake the disconnect would not be delivered if using _send().
147 _setStatus('disconnecting');
148 _deliver([message], false);
152 * Marks the start of a batch of application messages to be sent to the server
153 * in a single request, obtaining a single response containing (possibly) many
154 * application reply messages.
155 * Messages are held in a queue and not sent until {@link #endBatch()} is called.
156 * If startBatch() is called multiple times, then an equal number of endBatch()
157 * calls must be made to close and send the batch of messages.
160 this.startBatch = function()
166 * Marks the end of a batch of application messages to be sent to the server
167 * in a single request.
170 this.endBatch = function()
176 * Subscribes to the given channel, performing the given callback in the given scope
177 * when a message for the channel arrives.
178 * @param channel the channel to subscribe to
179 * @param scope the scope of the callback
180 * @param callback the callback to call when a message is delivered to the channel
181 * @param subscribeProps an object to be merged with the subscribe message
182 * @return the subscription handle to be passed to {@link #unsubscribe(object)}
184 this.subscribe = function(channel, scope, callback, subscribeProps)
186 var subscription = this.addListener(channel, scope, callback);
188 // Send the subscription message after the subscription registration to avoid
189 // races where the server would deliver a message to the subscribers, but here
190 // on the client the subscription has not been added yet to the data structures
191 var bayeuxMessage = {
192 channel: '/meta/subscribe',
193 subscription: channel
195 var message = $.extend({}, subscribeProps, bayeuxMessage);
202 * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
203 * @param subscription the subscription to unsubscribe.
205 this.unsubscribe = function(subscription, unsubscribeProps)
207 // Remove the local listener before sending the message
208 // This ensures that if the server fails, this client does not get notifications
209 this.removeListener(subscription);
210 var bayeuxMessage = {
211 channel: '/meta/unsubscribe',
212 subscription: subscription[0]
214 var message = $.extend({}, unsubscribeProps, bayeuxMessage);
219 * Publishes a message on the given channel, containing the given content.
220 * @param channel the channel to publish the message to
221 * @param content the content of the message
222 * @param publishProps an object to be merged with the publish message
224 this.publish = function(channel, content, publishProps)
226 var bayeuxMessage = {
230 var message = $.extend({}, publishProps, bayeuxMessage);
235 * Adds a listener for bayeux messages, performing the given callback in the given scope
236 * when a message for the given channel arrives.
237 * @param channel the channel the listener is interested to
238 * @param scope the scope of the callback
239 * @param callback the callback to call when a message is delivered to the channel
240 * @returns the subscription handle to be passed to {@link #removeListener(object)}
241 * @see #removeListener(object)
243 this.addListener = function(channel, scope, callback)
245 // The data structure is a map<channel, subscription[]>, where each subscription
246 // holds the callback to be called and its scope.
248 // Normalize arguments
260 var subscriptions = _listeners[channel];
264 _listeners[channel] = subscriptions;
266 // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
268 // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
270 // hc==3, a.join()=='a',,'c', a.length==3
271 var subscriptionIndex = subscriptions.push(subscription) - 1;
272 _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex);
274 // The subscription to allow removal of the listener is made of the channel and the index
275 return [channel, subscriptionIndex];
279 * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
280 * @param subscription the subscription to unsubscribe.
282 this.removeListener = function(subscription)
284 var subscriptions = _listeners[subscription[0]];
287 delete subscriptions[subscription[1]];
288 _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
293 * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
294 * {@link #subscribe(channel, scope, callback)}.
296 this.clearListeners = function()
302 * Returns a string representing the status of the bayeux communication with the comet server.
304 this.getStatus = function()
310 * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
311 * Default value is 1 second, which means if there is a persistent failure the retries will happen
312 * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
313 * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
314 * @param period the backoff period to set
315 * @see #getBackoffIncrement()
317 this.setBackoffIncrement = function(period)
319 _backoffIncrement = period;
323 * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
324 * @see #setBackoffIncrement(period)
326 this.getBackoffIncrement = function()
328 return _backoffIncrement;
332 * Returns the backoff period to wait before retrying an unsuccessful or failed message.
334 this.getBackoffPeriod = function()
340 * Sets the log level for console logging.
341 * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
342 * less verbose to more verbose.
343 * @param level the log level string
345 this.setLogLevel = function(level)
351 * Registers an extension whose callbacks are called for every incoming message
352 * (that comes from the server to this client implementation) and for every
353 * outgoing message (that originates from this client implementation for the
355 * The format of the extension object is the following:
358 * incoming: function(message) { ... },
359 * outgoing: function(message) { ... }
361 * Both properties are optional, but if they are present they will be called
362 * respectively for each incoming message and for each outgoing message.
364 * @param name the name of the extension
365 * @param extension the extension to register
366 * @return true if the extension was registered, false otherwise
367 * @see #unregisterExtension(name)
369 this.registerExtension = function(name, extension)
371 var existing = false;
372 for (var i = 0; i < _extensions.length; ++i)
374 var existingExtension = _extensions[i];
375 if (existingExtension.name == name)
387 _debug('Registered extension \'{}\'', name);
392 _info('Could not register extension with name \'{}\': another extension with the same name already exists');
398 * Unregister an extension previously registered with
399 * {@link #registerExtension(name, extension)}.
400 * @param name the name of the extension to unregister.
401 * @return true if the extension was unregistered, false otherwise
403 this.unregisterExtension = function(name)
405 var unregistered = false;
406 $.each(_extensions, function(index, extension)
408 if (extension.name == name)
410 _extensions.splice(index, 1);
412 _debug('Unregistered extension \'{}\'', name);
420 * Starts a the batch of messages to be sent in a single request.
421 * @see _endBatch(deliverMessages)
423 function _startBatch()
429 * Ends the batch of messages to be sent in a single request,
430 * optionally delivering messages present in the message queue depending
431 * on the given argument.
432 * @param deliverMessages whether to deliver the messages in the queue or not
435 function _endBatch(deliverMessages)
438 if (_batch < 0) _batch = 0;
439 if (deliverMessages && _batch == 0 && !_isDisconnected())
441 var messages = _messageQueue;
443 if (messages.length > 0) _deliver(messages, false);
447 function _nextMessageId()
453 * Converts the given response into an array of bayeux messages
454 * @param response the response to convert
455 * @return an array of bayeux messages obtained by converting the response
457 function _convertToMessages(response)
459 if (response === undefined) return [];
460 if (response instanceof Array) return response;
461 if (response instanceof String || typeof response == 'string') return eval('(' + response + ')');
462 if (response instanceof Object) return [response];
463 throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
466 function _setStatus(newStatus)
468 _debug('{} -> {}', _status, newStatus);
472 function _isDisconnected()
474 return _status == 'disconnecting' || _status == 'disconnected';
478 * Sends the initial handshake message
480 function _handshake(handshakeProps)
482 _debug('Starting handshake');
486 // This is needed because handshake and connect are async.
487 // It may happen that the application calls init() then subscribe()
488 // and the subscribe message is sent before the connect message, if
489 // the subscribe message is not held until the connect message is sent.
490 // So here we start a batch to hold temporarly any message until
491 // the connection is fully established.
495 // Save the original properties provided by the user
496 // Deep copy to avoid the user to be able to change them later
497 _handshakeProps = $.extend(true, {}, handshakeProps);
499 var bayeuxMessage = {
501 minimumVersion: '0.9',
502 channel: '/meta/handshake',
503 supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
505 // Do not allow the user to mess with the required properties,
506 // so merge first the user properties and *then* the bayeux message
507 var message = $.extend({}, handshakeProps, bayeuxMessage);
509 // We started a batch to hold the application messages,
510 // so here we must bypass it and deliver immediately.
511 _setStatus('handshaking');
512 _deliver([message], false);
515 function _findTransport(handshakeResponse)
517 var transportTypes = handshakeResponse.supportedConnectionTypes;
520 // If we are cross domain, check if the server supports it, that's the only option
521 if ($.inArray('callback-polling', transportTypes) >= 0) return _transport;
525 // Check if we can keep long-polling
526 if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
528 // The server does not support long-polling
529 if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
534 function _delayedHandshake()
536 _setStatus('handshaking');
537 _delayedSend(function()
539 _handshake(_handshakeProps);
543 function _delayedConnect()
545 _setStatus('connecting');
546 _delayedSend(function()
552 function _delayedSend(operation)
554 _cancelDelayedSend();
555 var delay = _backoff;
556 _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval);
557 if (_advice.interval && _advice.interval > 0)
558 delay += _advice.interval;
559 _scheduledSend = _setTimeout(operation, delay);
562 function _cancelDelayedSend()
564 if (_scheduledSend !== null) clearTimeout(_scheduledSend);
565 _scheduledSend = null;
568 function _setTimeout(funktion, delay)
570 return setTimeout(function()
578 _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
584 * Sends the connect message
588 _debug('Starting connect');
590 channel: '/meta/connect',
591 connectionType: _transport.getType()
593 _setStatus('connecting');
594 _deliver([message], true);
595 _setStatus('connected');
598 function _send(message)
601 _messageQueue.push(message);
603 _deliver([message], false);
607 * Delivers the messages to the comet server
608 * @param messages the array of messages to send
610 function _deliver(messages, comet)
612 // We must be sure that the messages have a clientId.
613 // This is not guaranteed since the handshake may take time to return
614 // (and hence the clientId is not known yet) and the application
615 // may create other messages.
616 $.each(messages, function(index, message)
618 message['id'] = _nextMessageId();
619 if (_clientId) message['clientId'] = _clientId;
620 messages[index] = _applyOutgoingExtensions(message);
627 onSuccess: function(request, response)
631 _handleSuccess.call(self, request, response, comet);
635 _debug('Exception during execution of success callback: {}', x);
638 onFailure: function(request, reason, exception)
642 _handleFailure.call(self, request, messages, reason, exception, comet);
646 _debug('Exception during execution of failure callback: {}', x);
650 _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
651 _transport.send(envelope, comet);
654 function _applyIncomingExtensions(message)
656 for (var i = 0; i < _extensions.length; ++i)
658 var extension = _extensions[i];
659 var callback = extension.extension.incoming;
660 if (callback && typeof callback === 'function')
662 _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
663 message = _applyExtension(extension.name, callback, message) || message;
669 function _applyOutgoingExtensions(message)
671 for (var i = 0; i < _extensions.length; ++i)
673 var extension = _extensions[i];
674 var callback = extension.extension.outgoing;
675 if (callback && typeof callback === 'function')
677 _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
678 message = _applyExtension(extension.name, callback, message) || message;
684 function _applyExtension(name, callback, message)
688 return callback(message);
692 _debug('Exception during execution of extension \'{}\': {}', name, x);
697 function _handleSuccess(request, response, comet)
699 var messages = _convertToMessages(response);
700 _debug('Received response {}', JSON.stringify(messages));
702 // Signal the transport it can deliver other queued requests
703 _transport.complete(request, true, comet);
705 for (var i = 0; i < messages.length; ++i)
707 var message = messages[i];
708 message = _applyIncomingExtensions(message);
710 if (message.advice) _advice = message.advice;
712 var channel = message.channel;
715 case '/meta/handshake':
716 _handshakeSuccess(message);
718 case '/meta/connect':
719 _connectSuccess(message);
721 case '/meta/disconnect':
722 _disconnectSuccess(message);
724 case '/meta/subscribe':
725 _subscribeSuccess(message);
727 case '/meta/unsubscribe':
728 _unsubscribeSuccess(message);
731 _messageSuccess(message);
737 function _handleFailure(request, messages, reason, exception, comet)
739 var xhr = request.xhr;
740 _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
742 // Signal the transport it can deliver other queued requests
743 _transport.complete(request, false, comet);
745 for (var i = 0; i < messages.length; ++i)
747 var message = messages[i];
748 var channel = message.channel;
751 case '/meta/handshake':
752 _handshakeFailure(xhr, message);
754 case '/meta/connect':
755 _connectFailure(xhr, message);
757 case '/meta/disconnect':
758 _disconnectFailure(xhr, message);
760 case '/meta/subscribe':
761 _subscribeFailure(xhr, message);
763 case '/meta/unsubscribe':
764 _unsubscribeFailure(xhr, message);
767 _messageFailure(xhr, message);
773 function _handshakeSuccess(message)
775 if (message.successful)
777 _debug('Handshake successful');
778 // Save clientId, figure out transport, then follow the advice to connect
779 _clientId = message.clientId;
781 var newTransport = _findTransport(message);
782 if (newTransport === null)
784 throw 'Could not agree on transport with server';
788 if (_transport.getType() != newTransport.getType())
790 _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
791 _transport = newTransport;
795 // Notify the listeners
796 // Here the new transport is in place, as well as the clientId, so
797 // the listener can perform a publish() if it wants, and the listeners
798 // are notified before the connect below.
799 _notifyListeners('/meta/handshake', message);
801 var action = _advice.reconnect ? _advice.reconnect : 'retry';
813 _debug('Handshake unsuccessful');
815 var retry = !_isDisconnected() && _advice.reconnect != 'none';
816 if (!retry) _setStatus('disconnected');
818 _notifyListeners('/meta/handshake', message);
819 _notifyListeners('/meta/unsuccessful', message);
821 // Only try again if we haven't been disconnected and
822 // the advice permits us to retry the handshake
826 _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
832 function _handshakeFailure(xhr, message)
834 _debug('Handshake failure');
837 var failureMessage = {
840 channel: '/meta/handshake',
849 var retry = !_isDisconnected() && _advice.reconnect != 'none';
850 if (!retry) _setStatus('disconnected');
852 _notifyListeners('/meta/handshake', failureMessage);
853 _notifyListeners('/meta/unsuccessful', failureMessage);
855 // Only try again if we haven't been disconnected and the
856 // advice permits us to try again
860 _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
865 function _connectSuccess(message)
867 var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
868 if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
870 if (message.successful)
872 _debug('Connect successful');
874 // End the batch and allow held messages from the application
875 // to go to the server (see _handshake() where we start the batch).
876 // The batch is ended before notifying the listeners, so that
877 // listeners can batch other cometd operations
880 // Notify the listeners after the status change but before the next connect
881 _notifyListeners('/meta/connect', message);
883 // Connect was successful.
884 // Normally, the advice will say "reconnect: 'retry', interval: 0"
885 // and the server will hold the request, so when a response returns
886 // we immediately call the server again (long polling)
895 _setStatus('disconnected');
901 _debug('Connect unsuccessful');
903 // Notify the listeners after the status change but before the next action
904 _notifyListeners('/meta/connect', message);
905 _notifyListeners('/meta/unsuccessful', message);
907 // Connect was not successful.
908 // This may happen when the server crashed, the current clientId
909 // will be invalid, and the server will ask to handshake again
917 // End the batch but do not deliver the messages until we connect successfully
924 _setStatus('disconnected');
930 function _connectFailure(xhr, message)
932 _debug('Connect failure');
935 var failureMessage = {
938 channel: '/meta/connect',
946 _notifyListeners('/meta/connect', failureMessage);
947 _notifyListeners('/meta/unsuccessful', failureMessage);
949 if (!_isDisconnected())
951 var action = _advice.reconnect ? _advice.reconnect : 'retry';
956 _debug('Connect failure, backing off and retrying in {} ms', _backoff);
967 _debug('Unrecognized reconnect value: {}', action);
973 function _disconnectSuccess(message)
975 if (message.successful)
977 _debug('Disconnect successful');
979 _notifyListeners('/meta/disconnect', message);
983 _debug('Disconnect unsuccessful');
985 _notifyListeners('/meta/disconnect', message);
986 _notifyListeners('/meta/usuccessful', message);
990 function _disconnect(abort)
992 _cancelDelayedSend();
993 if (abort) _transport.abort();
995 _setStatus('disconnected');
1001 function _disconnectFailure(xhr, message)
1003 _debug('Disconnect failure');
1006 var failureMessage = {
1009 channel: '/meta/disconnect',
1017 _notifyListeners('/meta/disconnect', failureMessage);
1018 _notifyListeners('/meta/unsuccessful', failureMessage);
1021 function _subscribeSuccess(message)
1023 if (message.successful)
1025 _debug('Subscribe successful');
1026 _notifyListeners('/meta/subscribe', message);
1030 _debug('Subscribe unsuccessful');
1031 _notifyListeners('/meta/subscribe', message);
1032 _notifyListeners('/meta/unsuccessful', message);
1036 function _subscribeFailure(xhr, message)
1038 _debug('Subscribe failure');
1040 var failureMessage = {
1043 channel: '/meta/subscribe',
1051 _notifyListeners('/meta/subscribe', failureMessage);
1052 _notifyListeners('/meta/unsuccessful', failureMessage);
1055 function _unsubscribeSuccess(message)
1057 if (message.successful)
1059 _debug('Unsubscribe successful');
1060 _notifyListeners('/meta/unsubscribe', message);
1064 _debug('Unsubscribe unsuccessful');
1065 _notifyListeners('/meta/unsubscribe', message);
1066 _notifyListeners('/meta/unsuccessful', message);
1070 function _unsubscribeFailure(xhr, message)
1072 _debug('Unsubscribe failure');
1074 var failureMessage = {
1077 channel: '/meta/unsubscribe',
1085 _notifyListeners('/meta/unsubscribe', failureMessage);
1086 _notifyListeners('/meta/unsuccessful', failureMessage);
1089 function _messageSuccess(message)
1091 if (message.successful === undefined)
1095 // It is a plain message, and not a bayeux meta message
1096 _notifyListeners(message.channel, message);
1100 _debug('Unknown message {}', JSON.stringify(message));
1105 if (message.successful)
1107 _debug('Publish successful');
1108 _notifyListeners('/meta/publish', message);
1112 _debug('Publish unsuccessful');
1113 _notifyListeners('/meta/publish', message);
1114 _notifyListeners('/meta/unsuccessful', message);
1119 function _messageFailure(xhr, message)
1121 _debug('Publish failure');
1123 var failureMessage = {
1126 channel: message.channel,
1134 _notifyListeners('/meta/publish', failureMessage);
1135 _notifyListeners('/meta/unsuccessful', failureMessage);
1138 function _notifyListeners(channel, message)
1140 // Notify direct listeners
1141 _notify(channel, message);
1143 // Notify the globbing listeners
1144 var channelParts = channel.split("/");
1145 var last = channelParts.length - 1;
1146 for (var i = last; i > 0; --i)
1148 var channelPart = channelParts.slice(0, i).join('/') + '/*';
1149 // We don't want to notify /foo/* if the channel is /foo/bar/baz,
1150 // so we stop at the first non recursive globbing
1151 if (i == last) _notify(channelPart, message);
1152 // Add the recursive globber and notify
1154 _notify(channelPart, message);
1158 function _notify(channel, message)
1160 var subscriptions = _listeners[channel];
1161 if (subscriptions && subscriptions.length > 0)
1163 for (var i = 0; i < subscriptions.length; ++i)
1165 var subscription = subscriptions[i];
1166 // Subscriptions may come and go, so the array may have 'holes'
1171 _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
1172 subscription.callback.call(subscription.scope, message);
1176 // Ignore exceptions from callbacks
1177 _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x);
1184 function _resetBackoff()
1189 function _increaseBackoff()
1191 if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
1194 var _error = this._error = function(text, args)
1196 _log('error', _format.apply(this, arguments));
1199 var _warn = this._warn = function(text, args)
1201 _log('warn', _format.apply(this, arguments));
1204 var _info = this._info = function(text, args)
1206 _log('info', _format.apply(this, arguments));
1209 var _debug = this._debug = function(text, args)
1211 _log('debug', _format.apply(this, arguments));
1214 function _log(level, text)
1216 var priority = _logPriorities[level];
1217 var configPriority = _logPriorities[_logLevel];
1218 if (!configPriority) configPriority = _logPriorities['info'];
1219 if (priority >= configPriority)
1221 if (window.console) window.console.log(text);
1225 function _format(text)
1227 var braces = /\{\}/g;
1231 while (braces.test(text))
1233 result += text.substr(start, braces.lastIndex - start - 2);
1234 var arg = arguments[++count];
1235 result += arg !== undefined ? arg : '{}';
1236 start = braces.lastIndex;
1238 result += text.substr(start, text.length - start);
1242 function newLongPollingTransport()
1244 return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
1247 function newCallbackPollingTransport()
1249 return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
1253 * Base object with the common functionality for transports.
1254 * The key responsibility is to allow at most 2 outstanding requests to the server,
1255 * to avoid that requests are sent behind a long poll.
1256 * To achieve this, we have one reserved request for the long poll, and all other
1257 * requests are serialized one after the other.
1259 var Transport = function(type)
1261 var _maxRequests = 2;
1262 var _requestIds = 0;
1263 var _cometRequest = null;
1267 this.getType = function()
1272 this.send = function(packet, comet)
1275 _cometSend(this, packet);
1277 _send(this, packet);
1280 function _cometSend(self, packet)
1282 if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
1284 var requestId = ++_requestIds;
1285 _debug('Beginning comet request {}', requestId);
1287 var request = {id: requestId};
1288 _debug('Delivering comet request {}', requestId);
1289 self.deliver(packet, request);
1290 _cometRequest = request;
1293 function _send(self, packet)
1295 var requestId = ++_requestIds;
1296 _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
1298 var request = {id: requestId};
1299 // Consider the comet request which should always be present
1300 if (_requests.length < _maxRequests - 1)
1302 _debug('Delivering request {}', requestId);
1303 self.deliver(packet, request);
1304 _requests.push(request);
1308 _packets.push([packet, request]);
1309 _debug('Queued request {}, {} queued requests', requestId, _packets.length);
1313 this.complete = function(request, success, comet)
1316 _cometComplete(request);
1318 _complete(this, request, success);
1321 function _cometComplete(request)
1323 var requestId = request.id;
1324 if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
1326 // Reset comet request
1327 _cometRequest = null;
1328 _debug('Ended comet request {}', requestId);
1331 function _complete(self, request, success)
1333 var requestId = request.id;
1334 var index = $.inArray(request, _requests);
1335 // The index can be negative the request has been aborted
1336 if (index >= 0) _requests.splice(index, 1);
1337 _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
1339 if (_packets.length > 0)
1341 var packet = _packets.shift();
1344 _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
1345 _send(self, packet[0]);
1349 _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length);
1350 // Keep the semantic of calling response callbacks asynchronously after the request
1351 setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0);
1356 this.abort = function()
1358 for (var i = 0; i < _requests.length; ++i)
1360 var request = _requests[i];
1361 _debug('Aborting request {}', request.id);
1362 if (request.xhr) request.xhr.abort();
1366 _debug('Aborting comet request {}', _cometRequest.id);
1367 if (_cometRequest.xhr) _cometRequest.xhr.abort();
1369 _cometRequest = null;
1375 var LongPollingTransport = function()
1377 this.deliver = function(packet, request)
1379 request.xhr = $.ajax({
1382 contentType: 'text/json;charset=UTF-8',
1383 beforeSend: function(xhr)
1385 xhr.setRequestHeader('Connection', 'Keep-Alive');
1388 data: JSON.stringify(packet.messages),
1389 success: function(response) { packet.onSuccess(request, response); },
1390 error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
1395 var CallbackPollingTransport = function()
1397 var _maxLength = 2000;
1398 this.deliver = function(packet, request)
1400 // Microsoft Internet Explorer has a 2083 URL max length
1401 // We must ensure that we stay within that length
1402 var messages = JSON.stringify(packet.messages);
1403 // Encode the messages because all brackets, quotes, commas, colons, etc
1404 // present in the JSON will be URL encoded, taking many more characters
1405 var urlLength = packet.url.length + encodeURI(messages).length;
1406 _debug('URL length: {}', urlLength);
1407 // Let's stay on the safe side and use 2000 instead of 2083
1408 // also because we did not count few characters among which
1409 // the parameter name 'message' and the parameter 'jsonp',
1410 // which sum up to about 50 chars
1411 if (urlLength > _maxLength)
1413 var x = packet.messages.length > 1 ?
1414 'Too many bayeux messages in the same batch resulting in message too big ' +
1415 '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() :
1416 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
1417 'for transport ' + this.getType();
1418 // Keep the semantic of calling response callbacks asynchronously after the request
1419 _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0);
1428 beforeSend: function(xhr)
1430 xhr.setRequestHeader('Connection', 'Keep-Alive');
1435 // In callback-polling, the content must be sent via the 'message' parameter
1438 success: function(response) { packet.onSuccess(request, response); },
1439 error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
1447 * The JS object that exposes the comet API to applications
1449 $.cometd = new $.Cometd(); // The default instance