]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Comet/jquery.comet.js
Updated default theme to use the CSS Sprites method for common icons
[quix0rs-gnu-social.git] / plugins / Comet / jquery.comet.js
1 /**
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:
23  *
24  * The above copyright notice and this permission notice shall be
25  * included in all copies or substantial portions of the Software.
26  *
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  * ----------------------------------------------------------------------------
35  * $Revision$ $Date$
36  */
37 (function($)
38 {
39     /**
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
44      * created via:
45      * <pre>
46      * var url2 = ...;
47      * var cometd2 = new $.Cometd();
48      * cometd2.init(url2);
49      * </pre>
50      */
51     $.Cometd = function(name)
52     {
53         var _name = name || 'default';
54         var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 };
55         var _logLevel = 'info';
56         var _url;
57         var _xd = false;
58         var _transport;
59         var _status = 'disconnected';
60         var _messageId = 0;
61         var _clientId = null;
62         var _batch = 0;
63         var _messageQueue = [];
64         var _listeners = {};
65         var _backoff = 0;
66         var _backoffIncrement = 1000;
67         var _maxBackoff = 60000;
68         var _scheduledSend = null;
69         var _extensions = [];
70         var _advice = {};
71         var _handshakeProps;
72
73         /**
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.
76          */
77         this.getName = function()
78         {
79             return _name;
80         };
81
82         /**
83          * Configures the initial comet communication with the comet server.
84          * @param cometURL the URL of the comet server
85          */
86         this.configure = function(cometURL)
87         {
88             _configure(cometURL);
89         };
90
91         function _configure(cometURL)
92         {
93             _url = cometURL;
94             _debug('Initializing comet with url: {}', _url);
95
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;
100
101             // Temporary setup a transport to send the initial handshake
102             // The transport may be changed as a result of handshake
103             if (_xd)
104                 _transport = newCallbackPollingTransport();
105             else
106                 _transport = newLongPollingTransport();
107             _debug('Initial transport is {}', _transport.getType());
108         };
109
110         /**
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)
117          */
118         this.init = function(cometURL, handshakeProps)
119         {
120             _configure(cometURL);
121             _handshake(handshakeProps);
122         };
123
124         /**
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
128          */
129         this.handshake = function(handshakeProps)
130         {
131             _handshake(handshakeProps);
132         };
133
134         /**
135          * Disconnects from the comet server.
136          * @param disconnectProps an object to be merged with the disconnect message
137          */
138         this.disconnect = function(disconnectProps)
139         {
140             var bayeuxMessage = {
141                 channel: '/meta/disconnect'
142             };
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);
149         };
150
151         /**
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.
158          * @see #endBatch()
159          */
160         this.startBatch = function()
161         {
162             _startBatch();
163         };
164
165         /**
166          * Marks the end of a batch of application messages to be sent to the server
167          * in a single request.
168          * @see #startBatch()
169          */
170         this.endBatch = function()
171         {
172             _endBatch(true);
173         };
174
175         /**
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)}
183          */
184         this.subscribe = function(channel, scope, callback, subscribeProps)
185         {
186             var subscription = this.addListener(channel, scope, callback);
187
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
194             };
195             var message = $.extend({}, subscribeProps, bayeuxMessage);
196             _send(message);
197
198             return subscription;
199         };
200
201         /**
202          * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
203          * @param subscription the subscription to unsubscribe.
204          */
205         this.unsubscribe = function(subscription, unsubscribeProps)
206         {
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]
213             };
214             var message = $.extend({}, unsubscribeProps, bayeuxMessage);
215             _send(message);
216         };
217
218         /**
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
223          */
224         this.publish = function(channel, content, publishProps)
225         {
226             var bayeuxMessage = {
227                 channel: channel,
228                 data: content
229             };
230             var message = $.extend({}, publishProps, bayeuxMessage);
231             _send(message);
232         };
233
234         /**
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)
242          */
243         this.addListener = function(channel, scope, callback)
244         {
245             // The data structure is a map<channel, subscription[]>, where each subscription
246             // holds the callback to be called and its scope.
247
248             // Normalize arguments
249             if (!callback)
250             {
251                 callback = scope;
252                 scope = undefined;
253             }
254
255             var subscription = {
256                 scope: scope,
257                 callback: callback
258             };
259
260             var subscriptions = _listeners[channel];
261             if (!subscriptions)
262             {
263                 subscriptions = [];
264                 _listeners[channel] = subscriptions;
265             }
266             // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
267             // Note that if:
268             // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
269             // then:
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);
273
274             // The subscription to allow removal of the listener is made of the channel and the index
275             return [channel, subscriptionIndex];
276         };
277
278         /**
279          * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
280          * @param subscription the subscription to unsubscribe.
281          */
282         this.removeListener = function(subscription)
283         {
284             var subscriptions = _listeners[subscription[0]];
285             if (subscriptions)
286             {
287                 delete subscriptions[subscription[1]];
288                 _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
289             }
290         };
291
292         /**
293          * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
294          * {@link #subscribe(channel, scope, callback)}.
295          */
296         this.clearListeners = function()
297         {
298             _listeners = {};
299         };
300
301         /**
302          * Returns a string representing the status of the bayeux communication with the comet server.
303          */
304         this.getStatus = function()
305         {
306             return _status;
307         };
308
309         /**
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()
316          */
317         this.setBackoffIncrement = function(period)
318         {
319             _backoffIncrement = period;
320         };
321
322         /**
323          * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
324          * @see #setBackoffIncrement(period)
325          */
326         this.getBackoffIncrement = function()
327         {
328             return _backoffIncrement;
329         };
330
331         /**
332          * Returns the backoff period to wait before retrying an unsuccessful or failed message.
333          */
334         this.getBackoffPeriod = function()
335         {
336             return _backoff;
337         };
338
339         /**
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
344          */
345         this.setLogLevel = function(level)
346         {
347             _logLevel = level;
348         };
349
350         /**
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
354          * server).
355          * The format of the extension object is the following:
356          * <pre>
357          * {
358          *     incoming: function(message) { ... },
359          *     outgoing: function(message) { ... }
360          * }
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.
363          * </pre>
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)
368          */
369         this.registerExtension = function(name, extension)
370         {
371             var existing = false;
372             for (var i = 0; i < _extensions.length; ++i)
373             {
374                 var existingExtension = _extensions[i];
375                 if (existingExtension.name == name)
376                 {
377                     existing = true;
378                     return false;
379                 }
380             }
381             if (!existing)
382             {
383                 _extensions.push({
384                     name: name,
385                     extension: extension
386                 });
387                 _debug('Registered extension \'{}\'', name);
388                 return true;
389             }
390             else
391             {
392                 _info('Could not register extension with name \'{}\': another extension with the same name already exists');
393                 return false;
394             }
395         };
396
397         /**
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
402          */
403         this.unregisterExtension = function(name)
404         {
405             var unregistered = false;
406             $.each(_extensions, function(index, extension)
407             {
408                 if (extension.name == name)
409                 {
410                     _extensions.splice(index, 1);
411                     unregistered = true;
412                     _debug('Unregistered extension \'{}\'', name);
413                     return false;
414                 }
415             });
416             return unregistered;
417         };
418
419         /**
420          * Starts a the batch of messages to be sent in a single request.
421          * @see _endBatch(deliverMessages)
422          */
423         function _startBatch()
424         {
425             ++_batch;
426         };
427
428         /**
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
433          * @see _startBatch()
434          */
435         function _endBatch(deliverMessages)
436         {
437             --_batch;
438             if (_batch < 0) _batch = 0;
439             if (deliverMessages && _batch == 0 && !_isDisconnected())
440             {
441                 var messages = _messageQueue;
442                 _messageQueue = [];
443                 if (messages.length > 0) _deliver(messages, false);
444             }
445         };
446
447         function _nextMessageId()
448         {
449             return ++_messageId;
450         };
451
452         /**
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
456          */
457         function _convertToMessages(response)
458         {
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);
464         };
465
466         function _setStatus(newStatus)
467         {
468             _debug('{} -> {}', _status, newStatus);
469             _status = newStatus;
470         };
471
472         function _isDisconnected()
473         {
474             return _status == 'disconnecting' || _status == 'disconnected';
475         };
476
477         /**
478          * Sends the initial handshake message
479          */
480         function _handshake(handshakeProps)
481         {
482             _debug('Starting handshake');
483             _clientId = null;
484
485             // Start a batch.
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.
492             _batch = 0;
493             _startBatch();
494
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);
498
499             var bayeuxMessage = {
500                 version: '1.0',
501                 minimumVersion: '0.9',
502                 channel: '/meta/handshake',
503                 supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
504             };
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);
508
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);
513         };
514
515         function _findTransport(handshakeResponse)
516         {
517             var transportTypes = handshakeResponse.supportedConnectionTypes;
518             if (_xd)
519             {
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;
522             }
523             else
524             {
525                 // Check if we can keep long-polling
526                 if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
527
528                 // The server does not support long-polling
529                 if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
530             }
531             return null;
532         };
533
534         function _delayedHandshake()
535         {
536             _setStatus('handshaking');
537             _delayedSend(function()
538             {
539                 _handshake(_handshakeProps);
540             });
541         };
542
543         function _delayedConnect()
544         {
545             _setStatus('connecting');
546             _delayedSend(function()
547             {
548                 _connect();
549             });
550         };
551
552         function _delayedSend(operation)
553         {
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);
560         };
561
562         function _cancelDelayedSend()
563         {
564             if (_scheduledSend !== null) clearTimeout(_scheduledSend);
565             _scheduledSend = null;
566         };
567
568         function _setTimeout(funktion, delay)
569         {
570             return setTimeout(function()
571             {
572                 try
573                 {
574                     funktion();
575                 }
576                 catch (x)
577                 {
578                     _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
579                 }
580             }, delay);
581         };
582
583         /**
584          * Sends the connect message
585          */
586         function _connect()
587         {
588             _debug('Starting connect');
589             var message = {
590                 channel: '/meta/connect',
591                 connectionType: _transport.getType()
592             };
593             _setStatus('connecting');
594             _deliver([message], true);
595             _setStatus('connected');
596         };
597
598         function _send(message)
599         {
600             if (_batch > 0)
601                 _messageQueue.push(message);
602             else
603                 _deliver([message], false);
604         };
605
606         /**
607          * Delivers the messages to the comet server
608          * @param messages the array of messages to send
609          */
610         function _deliver(messages, comet)
611         {
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)
617             {
618                 message['id'] = _nextMessageId();
619                 if (_clientId) message['clientId'] = _clientId;
620                 messages[index] = _applyOutgoingExtensions(message);
621             });
622
623             var self = this;
624             var envelope = {
625                 url: _url,
626                 messages: messages,
627                 onSuccess: function(request, response)
628                 {
629                     try
630                     {
631                         _handleSuccess.call(self, request, response, comet);
632                     }
633                     catch (x)
634                     {
635                         _debug('Exception during execution of success callback: {}', x);
636                     }
637                 },
638                 onFailure: function(request, reason, exception)
639                 {
640                     try
641                     {
642                         _handleFailure.call(self, request, messages, reason, exception, comet);
643                     }
644                     catch (x)
645                     {
646                         _debug('Exception during execution of failure callback: {}', x);
647                     }
648                 }
649             };
650             _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
651             _transport.send(envelope, comet);
652         };
653
654         function _applyIncomingExtensions(message)
655         {
656             for (var i = 0; i < _extensions.length; ++i)
657             {
658                 var extension = _extensions[i];
659                 var callback = extension.extension.incoming;
660                 if (callback && typeof callback === 'function')
661                 {
662                     _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
663                     message = _applyExtension(extension.name, callback, message) || message;
664                 }
665             }
666             return message;
667         };
668
669         function _applyOutgoingExtensions(message)
670         {
671             for (var i = 0; i < _extensions.length; ++i)
672             {
673                 var extension = _extensions[i];
674                 var callback = extension.extension.outgoing;
675                 if (callback && typeof callback === 'function')
676                 {
677                     _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
678                     message = _applyExtension(extension.name, callback, message) || message;
679                 }
680             }
681             return message;
682         };
683
684         function _applyExtension(name, callback, message)
685         {
686             try
687             {
688                 return callback(message);
689             }
690             catch (x)
691             {
692                 _debug('Exception during execution of extension \'{}\': {}', name, x);
693                 return message;
694             }
695         };
696
697         function _handleSuccess(request, response, comet)
698         {
699             var messages = _convertToMessages(response);
700             _debug('Received response {}', JSON.stringify(messages));
701
702             // Signal the transport it can deliver other queued requests
703             _transport.complete(request, true, comet);
704
705             for (var i = 0; i < messages.length; ++i)
706             {
707                 var message = messages[i];
708                 message = _applyIncomingExtensions(message);
709
710                 if (message.advice) _advice = message.advice;
711
712                 var channel = message.channel;
713                 switch (channel)
714                 {
715                     case '/meta/handshake':
716                         _handshakeSuccess(message);
717                         break;
718                     case '/meta/connect':
719                         _connectSuccess(message);
720                         break;
721                     case '/meta/disconnect':
722                         _disconnectSuccess(message);
723                         break;
724                     case '/meta/subscribe':
725                         _subscribeSuccess(message);
726                         break;
727                     case '/meta/unsubscribe':
728                         _unsubscribeSuccess(message);
729                         break;
730                     default:
731                         _messageSuccess(message);
732                         break;
733                 }
734             }
735         };
736
737         function _handleFailure(request, messages, reason, exception, comet)
738         {
739             var xhr = request.xhr;
740             _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
741
742             // Signal the transport it can deliver other queued requests
743             _transport.complete(request, false, comet);
744
745             for (var i = 0; i < messages.length; ++i)
746             {
747                 var message = messages[i];
748                 var channel = message.channel;
749                 switch (channel)
750                 {
751                     case '/meta/handshake':
752                         _handshakeFailure(xhr, message);
753                         break;
754                     case '/meta/connect':
755                         _connectFailure(xhr, message);
756                         break;
757                     case '/meta/disconnect':
758                         _disconnectFailure(xhr, message);
759                         break;
760                     case '/meta/subscribe':
761                         _subscribeFailure(xhr, message);
762                         break;
763                     case '/meta/unsubscribe':
764                         _unsubscribeFailure(xhr, message);
765                         break;
766                     default:
767                         _messageFailure(xhr, message);
768                         break;
769                 }
770             }
771         };
772
773         function _handshakeSuccess(message)
774         {
775             if (message.successful)
776             {
777                 _debug('Handshake successful');
778                 // Save clientId, figure out transport, then follow the advice to connect
779                 _clientId = message.clientId;
780
781                 var newTransport = _findTransport(message);
782                 if (newTransport === null)
783                 {
784                     throw 'Could not agree on transport with server';
785                 }
786                 else
787                 {
788                     if (_transport.getType() != newTransport.getType())
789                     {
790                         _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
791                         _transport = newTransport;
792                     }
793                 }
794
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);
800
801                 var action = _advice.reconnect ? _advice.reconnect : 'retry';
802                 switch (action)
803                 {
804                     case 'retry':
805                         _delayedConnect();
806                         break;
807                     default:
808                         break;
809                 }
810             }
811             else
812             {
813                 _debug('Handshake unsuccessful');
814
815                 var retry = !_isDisconnected() && _advice.reconnect != 'none';
816                 if (!retry) _setStatus('disconnected');
817
818                 _notifyListeners('/meta/handshake', message);
819                 _notifyListeners('/meta/unsuccessful', message);
820
821                 // Only try again if we haven't been disconnected and
822                 // the advice permits us to retry the handshake
823                 if (retry)
824                 {
825                     _increaseBackoff();
826                     _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
827                     _delayedHandshake();
828                 }
829             }
830         };
831
832         function _handshakeFailure(xhr, message)
833         {
834             _debug('Handshake failure');
835
836             // Notify listeners
837             var failureMessage = {
838                 successful: false,
839                 failure: true,
840                 channel: '/meta/handshake',
841                 request: message,
842                 xhr: xhr,
843                 advice: {
844                     action: 'retry',
845                     interval: _backoff
846                 }
847             };
848
849             var retry = !_isDisconnected() && _advice.reconnect != 'none';
850             if (!retry) _setStatus('disconnected');
851
852             _notifyListeners('/meta/handshake', failureMessage);
853             _notifyListeners('/meta/unsuccessful', failureMessage);
854
855             // Only try again if we haven't been disconnected and the
856             // advice permits us to try again
857             if (retry)
858             {
859                 _increaseBackoff();
860                 _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
861                 _delayedHandshake();
862             }
863         };
864
865         function _connectSuccess(message)
866         {
867             var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
868             if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
869
870             if (message.successful)
871             {
872                 _debug('Connect successful');
873
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
878                 _endBatch(true);
879
880                 // Notify the listeners after the status change but before the next connect
881                 _notifyListeners('/meta/connect', message);
882
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)
887                 switch (action)
888                 {
889                     case 'retry':
890                         _resetBackoff();
891                         _delayedConnect();
892                         break;
893                     default:
894                         _resetBackoff();
895                         _setStatus('disconnected');
896                         break;
897                 }
898             }
899             else
900             {
901                 _debug('Connect unsuccessful');
902
903                 // Notify the listeners after the status change but before the next action
904                 _notifyListeners('/meta/connect', message);
905                 _notifyListeners('/meta/unsuccessful', message);
906
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
910                 switch (action)
911                 {
912                     case 'retry':
913                         _increaseBackoff();
914                         _delayedConnect();
915                         break;
916                     case 'handshake':
917                         // End the batch but do not deliver the messages until we connect successfully
918                         _endBatch(false);
919                         _resetBackoff();
920                         _delayedHandshake();
921                         break;
922                     case 'none':
923                         _resetBackoff();
924                         _setStatus('disconnected');
925                         break;
926                 }
927             }
928         };
929
930         function _connectFailure(xhr, message)
931         {
932             _debug('Connect failure');
933
934             // Notify listeners
935             var failureMessage = {
936                 successful: false,
937                 failure: true,
938                 channel: '/meta/connect',
939                 request: message,
940                 xhr: xhr,
941                 advice: {
942                     action: 'retry',
943                     interval: _backoff
944                 }
945             };
946             _notifyListeners('/meta/connect', failureMessage);
947             _notifyListeners('/meta/unsuccessful', failureMessage);
948
949             if (!_isDisconnected())
950             {
951                 var action = _advice.reconnect ? _advice.reconnect : 'retry';
952                 switch (action)
953                 {
954                     case 'retry':
955                         _increaseBackoff();
956                         _debug('Connect failure, backing off and retrying in {} ms', _backoff);
957                         _delayedConnect();
958                         break;
959                     case 'handshake':
960                         _resetBackoff();
961                         _delayedHandshake();
962                         break;
963                     case 'none':
964                         _resetBackoff();
965                         break;
966                     default:
967                         _debug('Unrecognized reconnect value: {}', action);
968                         break;
969                 }
970             }
971         };
972
973         function _disconnectSuccess(message)
974         {
975             if (message.successful)
976             {
977                 _debug('Disconnect successful');
978                 _disconnect(false);
979                 _notifyListeners('/meta/disconnect', message);
980             }
981             else
982             {
983                 _debug('Disconnect unsuccessful');
984                 _disconnect(true);
985                 _notifyListeners('/meta/disconnect', message);
986                 _notifyListeners('/meta/usuccessful', message);
987             }
988         };
989
990         function _disconnect(abort)
991         {
992             _cancelDelayedSend();
993             if (abort) _transport.abort();
994             _clientId = null;
995             _setStatus('disconnected');
996             _batch = 0;
997             _messageQueue = [];
998             _resetBackoff();
999         };
1000
1001         function _disconnectFailure(xhr, message)
1002         {
1003             _debug('Disconnect failure');
1004             _disconnect(true);
1005
1006             var failureMessage = {
1007                 successful: false,
1008                 failure: true,
1009                 channel: '/meta/disconnect',
1010                 request: message,
1011                 xhr: xhr,
1012                 advice: {
1013                     action: 'none',
1014                     interval: 0
1015                 }
1016             };
1017             _notifyListeners('/meta/disconnect', failureMessage);
1018             _notifyListeners('/meta/unsuccessful', failureMessage);
1019         };
1020
1021         function _subscribeSuccess(message)
1022         {
1023             if (message.successful)
1024             {
1025                 _debug('Subscribe successful');
1026                 _notifyListeners('/meta/subscribe', message);
1027             }
1028             else
1029             {
1030                 _debug('Subscribe unsuccessful');
1031                 _notifyListeners('/meta/subscribe', message);
1032                 _notifyListeners('/meta/unsuccessful', message);
1033             }
1034         };
1035
1036         function _subscribeFailure(xhr, message)
1037         {
1038             _debug('Subscribe failure');
1039
1040             var failureMessage = {
1041                 successful: false,
1042                 failure: true,
1043                 channel: '/meta/subscribe',
1044                 request: message,
1045                 xhr: xhr,
1046                 advice: {
1047                     action: 'none',
1048                     interval: 0
1049                 }
1050             };
1051             _notifyListeners('/meta/subscribe', failureMessage);
1052             _notifyListeners('/meta/unsuccessful', failureMessage);
1053         };
1054
1055         function _unsubscribeSuccess(message)
1056         {
1057             if (message.successful)
1058             {
1059                 _debug('Unsubscribe successful');
1060                 _notifyListeners('/meta/unsubscribe', message);
1061             }
1062             else
1063             {
1064                 _debug('Unsubscribe unsuccessful');
1065                 _notifyListeners('/meta/unsubscribe', message);
1066                 _notifyListeners('/meta/unsuccessful', message);
1067             }
1068         };
1069
1070         function _unsubscribeFailure(xhr, message)
1071         {
1072             _debug('Unsubscribe failure');
1073
1074             var failureMessage = {
1075                 successful: false,
1076                 failure: true,
1077                 channel: '/meta/unsubscribe',
1078                 request: message,
1079                 xhr: xhr,
1080                 advice: {
1081                     action: 'none',
1082                     interval: 0
1083                 }
1084             };
1085             _notifyListeners('/meta/unsubscribe', failureMessage);
1086             _notifyListeners('/meta/unsuccessful', failureMessage);
1087         };
1088
1089         function _messageSuccess(message)
1090         {
1091             if (message.successful === undefined)
1092             {
1093                 if (message.data)
1094                 {
1095                     // It is a plain message, and not a bayeux meta message
1096                     _notifyListeners(message.channel, message);
1097                 }
1098                 else
1099                 {
1100                     _debug('Unknown message {}', JSON.stringify(message));
1101                 }
1102             }
1103             else
1104             {
1105                 if (message.successful)
1106                 {
1107                     _debug('Publish successful');
1108                     _notifyListeners('/meta/publish', message);
1109                 }
1110                 else
1111                 {
1112                     _debug('Publish unsuccessful');
1113                     _notifyListeners('/meta/publish', message);
1114                     _notifyListeners('/meta/unsuccessful', message);
1115                 }
1116             }
1117         };
1118
1119         function _messageFailure(xhr, message)
1120         {
1121             _debug('Publish failure');
1122
1123             var failureMessage = {
1124                 successful: false,
1125                 failure: true,
1126                 channel: message.channel,
1127                 request: message,
1128                 xhr: xhr,
1129                 advice: {
1130                     action: 'none',
1131                     interval: 0
1132                 }
1133             };
1134             _notifyListeners('/meta/publish', failureMessage);
1135             _notifyListeners('/meta/unsuccessful', failureMessage);
1136         };
1137
1138         function _notifyListeners(channel, message)
1139         {
1140             // Notify direct listeners
1141             _notify(channel, message);
1142
1143             // Notify the globbing listeners
1144             var channelParts = channel.split("/");
1145             var last = channelParts.length - 1;
1146             for (var i = last; i > 0; --i)
1147             {
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
1153                 channelPart += '*';
1154                 _notify(channelPart, message);
1155             }
1156         };
1157
1158         function _notify(channel, message)
1159         {
1160             var subscriptions = _listeners[channel];
1161             if (subscriptions && subscriptions.length > 0)
1162             {
1163                 for (var i = 0; i < subscriptions.length; ++i)
1164                 {
1165                     var subscription = subscriptions[i];
1166                     // Subscriptions may come and go, so the array may have 'holes'
1167                     if (subscription)
1168                     {
1169                         try
1170                         {
1171                             _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
1172                             subscription.callback.call(subscription.scope, message);
1173                         }
1174                         catch (x)
1175                         {
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);
1178                         }
1179                     }
1180                 }
1181             }
1182         };
1183
1184         function _resetBackoff()
1185         {
1186             _backoff = 0;
1187         };
1188
1189         function _increaseBackoff()
1190         {
1191             if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
1192         };
1193
1194         var _error = this._error = function(text, args)
1195         {
1196             _log('error', _format.apply(this, arguments));
1197         };
1198
1199         var _warn = this._warn = function(text, args)
1200         {
1201             _log('warn', _format.apply(this, arguments));
1202         };
1203
1204         var _info = this._info = function(text, args)
1205         {
1206             _log('info', _format.apply(this, arguments));
1207         };
1208
1209         var _debug = this._debug = function(text, args)
1210         {
1211             _log('debug', _format.apply(this, arguments));
1212         };
1213
1214         function _log(level, text)
1215         {
1216             var priority = _logPriorities[level];
1217             var configPriority = _logPriorities[_logLevel];
1218             if (!configPriority) configPriority = _logPriorities['info'];
1219             if (priority >= configPriority)
1220             {
1221                 if (window.console) window.console.log(text);
1222             }
1223         };
1224
1225         function _format(text)
1226         {
1227             var braces = /\{\}/g;
1228             var result = '';
1229             var start = 0;
1230             var count = 0;
1231             while (braces.test(text))
1232             {
1233                 result += text.substr(start, braces.lastIndex - start - 2);
1234                 var arg = arguments[++count];
1235                 result += arg !== undefined ? arg : '{}';
1236                 start = braces.lastIndex;
1237             }
1238             result += text.substr(start, text.length - start);
1239             return result;
1240         };
1241
1242         function newLongPollingTransport()
1243         {
1244             return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
1245         };
1246
1247         function newCallbackPollingTransport()
1248         {
1249             return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
1250         };
1251
1252         /**
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.
1258          */
1259         var Transport = function(type)
1260         {
1261             var _maxRequests = 2;
1262             var _requestIds = 0;
1263             var _cometRequest = null;
1264             var _requests = [];
1265             var _packets = [];
1266
1267             this.getType = function()
1268             {
1269                 return type;
1270             };
1271
1272             this.send = function(packet, comet)
1273             {
1274                 if (comet)
1275                     _cometSend(this, packet);
1276                 else
1277                     _send(this, packet);
1278             };
1279
1280             function _cometSend(self, packet)
1281             {
1282                 if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
1283
1284                 var requestId = ++_requestIds;
1285                 _debug('Beginning comet request {}', requestId);
1286
1287                 var request = {id: requestId};
1288                 _debug('Delivering comet request {}', requestId);
1289                 self.deliver(packet, request);
1290                 _cometRequest = request;
1291             };
1292
1293             function _send(self, packet)
1294             {
1295                 var requestId = ++_requestIds;
1296                 _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
1297
1298                 var request = {id: requestId};
1299                 // Consider the comet request which should always be present
1300                 if (_requests.length < _maxRequests - 1)
1301                 {
1302                     _debug('Delivering request {}', requestId);
1303                     self.deliver(packet, request);
1304                     _requests.push(request);
1305                 }
1306                 else
1307                 {
1308                     _packets.push([packet, request]);
1309                     _debug('Queued request {}, {} queued requests', requestId, _packets.length);
1310                 }
1311             };
1312
1313             this.complete = function(request, success, comet)
1314             {
1315                 if (comet)
1316                     _cometComplete(request);
1317                 else
1318                     _complete(this, request, success);
1319             };
1320
1321             function _cometComplete(request)
1322             {
1323                 var requestId = request.id;
1324                 if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
1325
1326                 // Reset comet request
1327                 _cometRequest = null;
1328                 _debug('Ended comet request {}', requestId);
1329             };
1330
1331             function _complete(self, request, success)
1332             {
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);
1338
1339                 if (_packets.length > 0)
1340                 {
1341                     var packet = _packets.shift();
1342                     if (success)
1343                     {
1344                         _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
1345                         _send(self, packet[0]);
1346                     }
1347                     else
1348                     {
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);
1352                     }
1353                 }
1354             };
1355
1356             this.abort = function()
1357             {
1358                 for (var i = 0; i < _requests.length; ++i)
1359                 {
1360                     var request = _requests[i];
1361                     _debug('Aborting request {}', request.id);
1362                     if (request.xhr) request.xhr.abort();
1363                 }
1364                 if (_cometRequest)
1365                 {
1366                     _debug('Aborting comet request {}', _cometRequest.id);
1367                     if (_cometRequest.xhr) _cometRequest.xhr.abort();
1368                 }
1369                 _cometRequest = null;
1370                 _requests = [];
1371                 _packets = [];
1372             };
1373         };
1374
1375         var LongPollingTransport = function()
1376         {
1377             this.deliver = function(packet, request)
1378             {
1379                 request.xhr = $.ajax({
1380                     url: packet.url,
1381                     type: 'POST',
1382                     contentType: 'text/json;charset=UTF-8',
1383                     beforeSend: function(xhr)
1384                     {
1385                         xhr.setRequestHeader('Connection', 'Keep-Alive');
1386                         return true;
1387                     },
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); }
1391                 });
1392             };
1393         };
1394
1395         var CallbackPollingTransport = function()
1396         {
1397             var _maxLength = 2000;
1398             this.deliver = function(packet, request)
1399             {
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)
1412                 {
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);
1420                 }
1421                 else
1422                 {
1423                     $.ajax({
1424                         url: packet.url,
1425                         type: 'GET',
1426                         dataType: 'jsonp',
1427                         jsonp: 'jsonp',
1428                         beforeSend: function(xhr)
1429                         {
1430                             xhr.setRequestHeader('Connection', 'Keep-Alive');
1431                             return true;
1432                         },
1433                         data:
1434                         {
1435                             // In callback-polling, the content must be sent via the 'message' parameter
1436                             message: messages
1437                         },
1438                         success: function(response) { packet.onSuccess(request, response); },
1439                         error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
1440                     });
1441                 }
1442             };
1443         };
1444     };
1445
1446     /**
1447      * The JS object that exposes the comet API to applications
1448      */
1449     $.cometd = new $.Cometd(); // The default instance
1450
1451 })(jQuery);