123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452 |
- /**
- * Copyright 2008 Mort Bay Consulting Pty. Ltd.
- * Dual licensed under the Apache License 2.0 and the MIT license.
- * ----------------------------------------------------------------------------
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http: *www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ----------------------------------------------------------------------------
- * Licensed under the MIT license;
- * Permission is hereby granted, free of charge, to any person obtaining
- * a copy of this software and associated documentation files (the
- * "Software"), to deal in the Software without restriction, including
- * without limitation the rights to use, copy, modify, merge, publish,
- * distribute, sublicense, and/or sell copies of the Software, and to
- * permit persons to whom the Software is furnished to do so, subject to
- * the following conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- * ----------------------------------------------------------------------------
- * $Revision$ $Date$
- */
- (function($)
- {
- /**
- * The constructor for a Comet object.
- * There is a default Comet instance already created at the variable <code>$.cometd</code>,
- * and hence that can be used to start a comet conversation with a server.
- * In the rare case a page needs more than one comet conversation, a new instance can be
- * created via:
- * <pre>
- * var url2 = ...;
- * var cometd2 = new $.Cometd();
- * cometd2.init(url2);
- * </pre>
- */
- $.Cometd = function(name)
- {
- var _name = name || 'default';
- var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 };
- var _logLevel = 'info';
- var _url;
- var _xd = false;
- var _transport;
- var _status = 'disconnected';
- var _messageId = 0;
- var _clientId = null;
- var _batch = 0;
- var _messageQueue = [];
- var _listeners = {};
- var _backoff = 0;
- var _backoffIncrement = 1000;
- var _maxBackoff = 60000;
- var _scheduledSend = null;
- var _extensions = [];
- var _advice = {};
- var _handshakeProps;
- /**
- * Returns the name assigned to this Comet object, or the string 'default'
- * if no name has been explicitely passed as parameter to the constructor.
- */
- this.getName = function()
- {
- return _name;
- };
- /**
- * Configures the initial comet communication with the comet server.
- * @param cometURL the URL of the comet server
- */
- this.configure = function(cometURL)
- {
- _configure(cometURL);
- };
- function _configure(cometURL)
- {
- _url = cometURL;
- _debug('Initializing comet with url: {}', _url);
- // Check immediately if we're cross domain
- // If cross domain, the handshake must not send the long polling transport type
- var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL);
- if (urlParts[3]) _xd = urlParts[3] != location.host;
- // Temporary setup a transport to send the initial handshake
- // The transport may be changed as a result of handshake
- if (_xd)
- _transport = newCallbackPollingTransport();
- else
- _transport = newLongPollingTransport();
- _debug('Initial transport is {}', _transport.getType());
- };
- /**
- * Configures and establishes the comet communication with the comet server
- * via a handshake and a subsequent connect.
- * @param cometURL the URL of the comet server
- * @param handshakeProps an object to be merged with the handshake message
- * @see #configure(cometURL)
- * @see #handshake(handshakeProps)
- */
- this.init = function(cometURL, handshakeProps)
- {
- _configure(cometURL);
- _handshake(handshakeProps);
- };
- /**
- * Establishes the comet communication with the comet server
- * via a handshake and a subsequent connect.
- * @param handshakeProps an object to be merged with the handshake message
- */
- this.handshake = function(handshakeProps)
- {
- _handshake(handshakeProps);
- };
- /**
- * Disconnects from the comet server.
- * @param disconnectProps an object to be merged with the disconnect message
- */
- this.disconnect = function(disconnectProps)
- {
- var bayeuxMessage = {
- channel: '/meta/disconnect'
- };
- var message = $.extend({}, disconnectProps, bayeuxMessage);
- // Deliver immediately
- // The handshake and connect mechanism make use of startBatch(), and in case
- // of a failed handshake the disconnect would not be delivered if using _send().
- _setStatus('disconnecting');
- _deliver([message], false);
- };
- /**
- * Marks the start of a batch of application messages to be sent to the server
- * in a single request, obtaining a single response containing (possibly) many
- * application reply messages.
- * Messages are held in a queue and not sent until {@link #endBatch()} is called.
- * If startBatch() is called multiple times, then an equal number of endBatch()
- * calls must be made to close and send the batch of messages.
- * @see #endBatch()
- */
- this.startBatch = function()
- {
- _startBatch();
- };
- /**
- * Marks the end of a batch of application messages to be sent to the server
- * in a single request.
- * @see #startBatch()
- */
- this.endBatch = function()
- {
- _endBatch(true);
- };
- /**
- * Subscribes to the given channel, performing the given callback in the given scope
- * when a message for the channel arrives.
- * @param channel the channel to subscribe to
- * @param scope the scope of the callback
- * @param callback the callback to call when a message is delivered to the channel
- * @param subscribeProps an object to be merged with the subscribe message
- * @return the subscription handle to be passed to {@link #unsubscribe(object)}
- */
- this.subscribe = function(channel, scope, callback, subscribeProps)
- {
- var subscription = this.addListener(channel, scope, callback);
- // Send the subscription message after the subscription registration to avoid
- // races where the server would deliver a message to the subscribers, but here
- // on the client the subscription has not been added yet to the data structures
- var bayeuxMessage = {
- channel: '/meta/subscribe',
- subscription: channel
- };
- var message = $.extend({}, subscribeProps, bayeuxMessage);
- _send(message);
- return subscription;
- };
- /**
- * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
- * @param subscription the subscription to unsubscribe.
- */
- this.unsubscribe = function(subscription, unsubscribeProps)
- {
- // Remove the local listener before sending the message
- // This ensures that if the server fails, this client does not get notifications
- this.removeListener(subscription);
- var bayeuxMessage = {
- channel: '/meta/unsubscribe',
- subscription: subscription[0]
- };
- var message = $.extend({}, unsubscribeProps, bayeuxMessage);
- _send(message);
- };
- /**
- * Publishes a message on the given channel, containing the given content.
- * @param channel the channel to publish the message to
- * @param content the content of the message
- * @param publishProps an object to be merged with the publish message
- */
- this.publish = function(channel, content, publishProps)
- {
- var bayeuxMessage = {
- channel: channel,
- data: content
- };
- var message = $.extend({}, publishProps, bayeuxMessage);
- _send(message);
- };
- /**
- * Adds a listener for bayeux messages, performing the given callback in the given scope
- * when a message for the given channel arrives.
- * @param channel the channel the listener is interested to
- * @param scope the scope of the callback
- * @param callback the callback to call when a message is delivered to the channel
- * @returns the subscription handle to be passed to {@link #removeListener(object)}
- * @see #removeListener(object)
- */
- this.addListener = function(channel, scope, callback)
- {
- // The data structure is a map<channel, subscription[]>, where each subscription
- // holds the callback to be called and its scope.
- // Normalize arguments
- if (!callback)
- {
- callback = scope;
- scope = undefined;
- }
- var subscription = {
- scope: scope,
- callback: callback
- };
- var subscriptions = _listeners[channel];
- if (!subscriptions)
- {
- subscriptions = [];
- _listeners[channel] = subscriptions;
- }
- // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
- // Note that if:
- // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
- // then:
- // hc==3, a.join()=='a',,'c', a.length==3
- var subscriptionIndex = subscriptions.push(subscription) - 1;
- _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex);
- // The subscription to allow removal of the listener is made of the channel and the index
- return [channel, subscriptionIndex];
- };
- /**
- * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
- * @param subscription the subscription to unsubscribe.
- */
- this.removeListener = function(subscription)
- {
- var subscriptions = _listeners[subscription[0]];
- if (subscriptions)
- {
- delete subscriptions[subscription[1]];
- _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
- }
- };
- /**
- * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
- * {@link #subscribe(channel, scope, callback)}.
- */
- this.clearListeners = function()
- {
- _listeners = {};
- };
- /**
- * Returns a string representing the status of the bayeux communication with the comet server.
- */
- this.getStatus = function()
- {
- return _status;
- };
- /**
- * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
- * Default value is 1 second, which means if there is a persistent failure the retries will happen
- * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
- * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
- * @param period the backoff period to set
- * @see #getBackoffIncrement()
- */
- this.setBackoffIncrement = function(period)
- {
- _backoffIncrement = period;
- };
- /**
- * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
- * @see #setBackoffIncrement(period)
- */
- this.getBackoffIncrement = function()
- {
- return _backoffIncrement;
- };
- /**
- * Returns the backoff period to wait before retrying an unsuccessful or failed message.
- */
- this.getBackoffPeriod = function()
- {
- return _backoff;
- };
- /**
- * Sets the log level for console logging.
- * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
- * less verbose to more verbose.
- * @param level the log level string
- */
- this.setLogLevel = function(level)
- {
- _logLevel = level;
- };
- /**
- * Registers an extension whose callbacks are called for every incoming message
- * (that comes from the server to this client implementation) and for every
- * outgoing message (that originates from this client implementation for the
- * server).
- * The format of the extension object is the following:
- * <pre>
- * {
- * incoming: function(message) { ... },
- * outgoing: function(message) { ... }
- * }
- * Both properties are optional, but if they are present they will be called
- * respectively for each incoming message and for each outgoing message.
- * </pre>
- * @param name the name of the extension
- * @param extension the extension to register
- * @return true if the extension was registered, false otherwise
- * @see #unregisterExtension(name)
- */
- this.registerExtension = function(name, extension)
- {
- var existing = false;
- for (var i = 0; i < _extensions.length; ++i)
- {
- var existingExtension = _extensions[i];
- if (existingExtension.name == name)
- {
- existing = true;
- return false;
- }
- }
- if (!existing)
- {
- _extensions.push({
- name: name,
- extension: extension
- });
- _debug('Registered extension \'{}\'', name);
- return true;
- }
- else
- {
- _info('Could not register extension with name \'{}\': another extension with the same name already exists');
- return false;
- }
- };
- /**
- * Unregister an extension previously registered with
- * {@link #registerExtension(name, extension)}.
- * @param name the name of the extension to unregister.
- * @return true if the extension was unregistered, false otherwise
- */
- this.unregisterExtension = function(name)
- {
- var unregistered = false;
- $.each(_extensions, function(index, extension)
- {
- if (extension.name == name)
- {
- _extensions.splice(index, 1);
- unregistered = true;
- _debug('Unregistered extension \'{}\'', name);
- return false;
- }
- });
- return unregistered;
- };
- /**
- * Starts a the batch of messages to be sent in a single request.
- * @see _endBatch(deliverMessages)
- */
- function _startBatch()
- {
- ++_batch;
- };
- /**
- * Ends the batch of messages to be sent in a single request,
- * optionally delivering messages present in the message queue depending
- * on the given argument.
- * @param deliverMessages whether to deliver the messages in the queue or not
- * @see _startBatch()
- */
- function _endBatch(deliverMessages)
- {
- --_batch;
- if (_batch < 0) _batch = 0;
- if (deliverMessages && _batch == 0 && !_isDisconnected())
- {
- var messages = _messageQueue;
- _messageQueue = [];
- if (messages.length > 0) _deliver(messages, false);
- }
- };
- function _nextMessageId()
- {
- return ++_messageId;
- };
- /**
- * Converts the given response into an array of bayeux messages
- * @param response the response to convert
- * @return an array of bayeux messages obtained by converting the response
- */
- function _convertToMessages(response)
- {
- if (response === undefined) return [];
- if (response instanceof Array) return response;
- if (response instanceof String || typeof response == 'string') return eval('(' + response + ')');
- if (response instanceof Object) return [response];
- throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
- };
- function _setStatus(newStatus)
- {
- _debug('{} -> {}', _status, newStatus);
- _status = newStatus;
- };
- function _isDisconnected()
- {
- return _status == 'disconnecting' || _status == 'disconnected';
- };
- /**
- * Sends the initial handshake message
- */
- function _handshake(handshakeProps)
- {
- _debug('Starting handshake');
- _clientId = null;
- // Start a batch.
- // This is needed because handshake and connect are async.
- // It may happen that the application calls init() then subscribe()
- // and the subscribe message is sent before the connect message, if
- // the subscribe message is not held until the connect message is sent.
- // So here we start a batch to hold temporarly any message until
- // the connection is fully established.
- _batch = 0;
- _startBatch();
- // Save the original properties provided by the user
- // Deep copy to avoid the user to be able to change them later
- _handshakeProps = $.extend(true, {}, handshakeProps);
- var bayeuxMessage = {
- version: '1.0',
- minimumVersion: '0.9',
- channel: '/meta/handshake',
- supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
- };
- // Do not allow the user to mess with the required properties,
- // so merge first the user properties and *then* the bayeux message
- var message = $.extend({}, handshakeProps, bayeuxMessage);
- // We started a batch to hold the application messages,
- // so here we must bypass it and deliver immediately.
- _setStatus('handshaking');
- _deliver([message], false);
- };
- function _findTransport(handshakeResponse)
- {
- var transportTypes = handshakeResponse.supportedConnectionTypes;
- if (_xd)
- {
- // If we are cross domain, check if the server supports it, that's the only option
- if ($.inArray('callback-polling', transportTypes) >= 0) return _transport;
- }
- else
- {
- // Check if we can keep long-polling
- if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
- // The server does not support long-polling
- if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
- }
- return null;
- };
- function _delayedHandshake()
- {
- _setStatus('handshaking');
- _delayedSend(function()
- {
- _handshake(_handshakeProps);
- });
- };
- function _delayedConnect()
- {
- _setStatus('connecting');
- _delayedSend(function()
- {
- _connect();
- });
- };
- function _delayedSend(operation)
- {
- _cancelDelayedSend();
- var delay = _backoff;
- _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval);
- if (_advice.interval && _advice.interval > 0)
- delay += _advice.interval;
- _scheduledSend = _setTimeout(operation, delay);
- };
- function _cancelDelayedSend()
- {
- if (_scheduledSend !== null) clearTimeout(_scheduledSend);
- _scheduledSend = null;
- };
- function _setTimeout(funktion, delay)
- {
- return setTimeout(function()
- {
- try
- {
- funktion();
- }
- catch (x)
- {
- _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
- }
- }, delay);
- };
- /**
- * Sends the connect message
- */
- function _connect()
- {
- _debug('Starting connect');
- var message = {
- channel: '/meta/connect',
- connectionType: _transport.getType()
- };
- _setStatus('connecting');
- _deliver([message], true);
- _setStatus('connected');
- };
- function _send(message)
- {
- if (_batch > 0)
- _messageQueue.push(message);
- else
- _deliver([message], false);
- };
- /**
- * Delivers the messages to the comet server
- * @param messages the array of messages to send
- */
- function _deliver(messages, comet)
- {
- // We must be sure that the messages have a clientId.
- // This is not guaranteed since the handshake may take time to return
- // (and hence the clientId is not known yet) and the application
- // may create other messages.
- $.each(messages, function(index, message)
- {
- message['id'] = _nextMessageId();
- if (_clientId) message['clientId'] = _clientId;
- messages[index] = _applyOutgoingExtensions(message);
- });
- var self = this;
- var envelope = {
- url: _url,
- messages: messages,
- onSuccess: function(request, response)
- {
- try
- {
- _handleSuccess.call(self, request, response, comet);
- }
- catch (x)
- {
- _debug('Exception during execution of success callback: {}', x);
- }
- },
- onFailure: function(request, reason, exception)
- {
- try
- {
- _handleFailure.call(self, request, messages, reason, exception, comet);
- }
- catch (x)
- {
- _debug('Exception during execution of failure callback: {}', x);
- }
- }
- };
- _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
- _transport.send(envelope, comet);
- };
- function _applyIncomingExtensions(message)
- {
- for (var i = 0; i < _extensions.length; ++i)
- {
- var extension = _extensions[i];
- var callback = extension.extension.incoming;
- if (callback && typeof callback === 'function')
- {
- _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
- message = _applyExtension(extension.name, callback, message) || message;
- }
- }
- return message;
- };
- function _applyOutgoingExtensions(message)
- {
- for (var i = 0; i < _extensions.length; ++i)
- {
- var extension = _extensions[i];
- var callback = extension.extension.outgoing;
- if (callback && typeof callback === 'function')
- {
- _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
- message = _applyExtension(extension.name, callback, message) || message;
- }
- }
- return message;
- };
- function _applyExtension(name, callback, message)
- {
- try
- {
- return callback(message);
- }
- catch (x)
- {
- _debug('Exception during execution of extension \'{}\': {}', name, x);
- return message;
- }
- };
- function _handleSuccess(request, response, comet)
- {
- var messages = _convertToMessages(response);
- _debug('Received response {}', JSON.stringify(messages));
- // Signal the transport it can deliver other queued requests
- _transport.complete(request, true, comet);
- for (var i = 0; i < messages.length; ++i)
- {
- var message = messages[i];
- message = _applyIncomingExtensions(message);
- if (message.advice) _advice = message.advice;
- var channel = message.channel;
- switch (channel)
- {
- case '/meta/handshake':
- _handshakeSuccess(message);
- break;
- case '/meta/connect':
- _connectSuccess(message);
- break;
- case '/meta/disconnect':
- _disconnectSuccess(message);
- break;
- case '/meta/subscribe':
- _subscribeSuccess(message);
- break;
- case '/meta/unsubscribe':
- _unsubscribeSuccess(message);
- break;
- default:
- _messageSuccess(message);
- break;
- }
- }
- };
- function _handleFailure(request, messages, reason, exception, comet)
- {
- var xhr = request.xhr;
- _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
- // Signal the transport it can deliver other queued requests
- _transport.complete(request, false, comet);
- for (var i = 0; i < messages.length; ++i)
- {
- var message = messages[i];
- var channel = message.channel;
- switch (channel)
- {
- case '/meta/handshake':
- _handshakeFailure(xhr, message);
- break;
- case '/meta/connect':
- _connectFailure(xhr, message);
- break;
- case '/meta/disconnect':
- _disconnectFailure(xhr, message);
- break;
- case '/meta/subscribe':
- _subscribeFailure(xhr, message);
- break;
- case '/meta/unsubscribe':
- _unsubscribeFailure(xhr, message);
- break;
- default:
- _messageFailure(xhr, message);
- break;
- }
- }
- };
- function _handshakeSuccess(message)
- {
- if (message.successful)
- {
- _debug('Handshake successful');
- // Save clientId, figure out transport, then follow the advice to connect
- _clientId = message.clientId;
- var newTransport = _findTransport(message);
- if (newTransport === null)
- {
- throw 'Could not agree on transport with server';
- }
- else
- {
- if (_transport.getType() != newTransport.getType())
- {
- _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
- _transport = newTransport;
- }
- }
- // Notify the listeners
- // Here the new transport is in place, as well as the clientId, so
- // the listener can perform a publish() if it wants, and the listeners
- // are notified before the connect below.
- _notifyListeners('/meta/handshake', message);
- var action = _advice.reconnect ? _advice.reconnect : 'retry';
- switch (action)
- {
- case 'retry':
- _delayedConnect();
- break;
- default:
- break;
- }
- }
- else
- {
- _debug('Handshake unsuccessful');
- var retry = !_isDisconnected() && _advice.reconnect != 'none';
- if (!retry) _setStatus('disconnected');
- _notifyListeners('/meta/handshake', message);
- _notifyListeners('/meta/unsuccessful', message);
- // Only try again if we haven't been disconnected and
- // the advice permits us to retry the handshake
- if (retry)
- {
- _increaseBackoff();
- _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
- _delayedHandshake();
- }
- }
- };
- function _handshakeFailure(xhr, message)
- {
- _debug('Handshake failure');
- // Notify listeners
- var failureMessage = {
- successful: false,
- failure: true,
- channel: '/meta/handshake',
- request: message,
- xhr: xhr,
- advice: {
- action: 'retry',
- interval: _backoff
- }
- };
- var retry = !_isDisconnected() && _advice.reconnect != 'none';
- if (!retry) _setStatus('disconnected');
- _notifyListeners('/meta/handshake', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- // Only try again if we haven't been disconnected and the
- // advice permits us to try again
- if (retry)
- {
- _increaseBackoff();
- _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
- _delayedHandshake();
- }
- };
- function _connectSuccess(message)
- {
- var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
- if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
- if (message.successful)
- {
- _debug('Connect successful');
- // End the batch and allow held messages from the application
- // to go to the server (see _handshake() where we start the batch).
- // The batch is ended before notifying the listeners, so that
- // listeners can batch other cometd operations
- _endBatch(true);
- // Notify the listeners after the status change but before the next connect
- _notifyListeners('/meta/connect', message);
- // Connect was successful.
- // Normally, the advice will say "reconnect: 'retry', interval: 0"
- // and the server will hold the request, so when a response returns
- // we immediately call the server again (long polling)
- switch (action)
- {
- case 'retry':
- _resetBackoff();
- _delayedConnect();
- break;
- default:
- _resetBackoff();
- _setStatus('disconnected');
- break;
- }
- }
- else
- {
- _debug('Connect unsuccessful');
- // Notify the listeners after the status change but before the next action
- _notifyListeners('/meta/connect', message);
- _notifyListeners('/meta/unsuccessful', message);
- // Connect was not successful.
- // This may happen when the server crashed, the current clientId
- // will be invalid, and the server will ask to handshake again
- switch (action)
- {
- case 'retry':
- _increaseBackoff();
- _delayedConnect();
- break;
- case 'handshake':
- // End the batch but do not deliver the messages until we connect successfully
- _endBatch(false);
- _resetBackoff();
- _delayedHandshake();
- break;
- case 'none':
- _resetBackoff();
- _setStatus('disconnected');
- break;
- }
- }
- };
- function _connectFailure(xhr, message)
- {
- _debug('Connect failure');
- // Notify listeners
- var failureMessage = {
- successful: false,
- failure: true,
- channel: '/meta/connect',
- request: message,
- xhr: xhr,
- advice: {
- action: 'retry',
- interval: _backoff
- }
- };
- _notifyListeners('/meta/connect', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- if (!_isDisconnected())
- {
- var action = _advice.reconnect ? _advice.reconnect : 'retry';
- switch (action)
- {
- case 'retry':
- _increaseBackoff();
- _debug('Connect failure, backing off and retrying in {} ms', _backoff);
- _delayedConnect();
- break;
- case 'handshake':
- _resetBackoff();
- _delayedHandshake();
- break;
- case 'none':
- _resetBackoff();
- break;
- default:
- _debug('Unrecognized reconnect value: {}', action);
- break;
- }
- }
- };
- function _disconnectSuccess(message)
- {
- if (message.successful)
- {
- _debug('Disconnect successful');
- _disconnect(false);
- _notifyListeners('/meta/disconnect', message);
- }
- else
- {
- _debug('Disconnect unsuccessful');
- _disconnect(true);
- _notifyListeners('/meta/disconnect', message);
- _notifyListeners('/meta/usuccessful', message);
- }
- };
- function _disconnect(abort)
- {
- _cancelDelayedSend();
- if (abort) _transport.abort();
- _clientId = null;
- _setStatus('disconnected');
- _batch = 0;
- _messageQueue = [];
- _resetBackoff();
- };
- function _disconnectFailure(xhr, message)
- {
- _debug('Disconnect failure');
- _disconnect(true);
- var failureMessage = {
- successful: false,
- failure: true,
- channel: '/meta/disconnect',
- request: message,
- xhr: xhr,
- advice: {
- action: 'none',
- interval: 0
- }
- };
- _notifyListeners('/meta/disconnect', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- };
- function _subscribeSuccess(message)
- {
- if (message.successful)
- {
- _debug('Subscribe successful');
- _notifyListeners('/meta/subscribe', message);
- }
- else
- {
- _debug('Subscribe unsuccessful');
- _notifyListeners('/meta/subscribe', message);
- _notifyListeners('/meta/unsuccessful', message);
- }
- };
- function _subscribeFailure(xhr, message)
- {
- _debug('Subscribe failure');
- var failureMessage = {
- successful: false,
- failure: true,
- channel: '/meta/subscribe',
- request: message,
- xhr: xhr,
- advice: {
- action: 'none',
- interval: 0
- }
- };
- _notifyListeners('/meta/subscribe', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- };
- function _unsubscribeSuccess(message)
- {
- if (message.successful)
- {
- _debug('Unsubscribe successful');
- _notifyListeners('/meta/unsubscribe', message);
- }
- else
- {
- _debug('Unsubscribe unsuccessful');
- _notifyListeners('/meta/unsubscribe', message);
- _notifyListeners('/meta/unsuccessful', message);
- }
- };
- function _unsubscribeFailure(xhr, message)
- {
- _debug('Unsubscribe failure');
- var failureMessage = {
- successful: false,
- failure: true,
- channel: '/meta/unsubscribe',
- request: message,
- xhr: xhr,
- advice: {
- action: 'none',
- interval: 0
- }
- };
- _notifyListeners('/meta/unsubscribe', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- };
- function _messageSuccess(message)
- {
- if (message.successful === undefined)
- {
- if (message.data)
- {
- // It is a plain message, and not a bayeux meta message
- _notifyListeners(message.channel, message);
- }
- else
- {
- _debug('Unknown message {}', JSON.stringify(message));
- }
- }
- else
- {
- if (message.successful)
- {
- _debug('Publish successful');
- _notifyListeners('/meta/publish', message);
- }
- else
- {
- _debug('Publish unsuccessful');
- _notifyListeners('/meta/publish', message);
- _notifyListeners('/meta/unsuccessful', message);
- }
- }
- };
- function _messageFailure(xhr, message)
- {
- _debug('Publish failure');
- var failureMessage = {
- successful: false,
- failure: true,
- channel: message.channel,
- request: message,
- xhr: xhr,
- advice: {
- action: 'none',
- interval: 0
- }
- };
- _notifyListeners('/meta/publish', failureMessage);
- _notifyListeners('/meta/unsuccessful', failureMessage);
- };
- function _notifyListeners(channel, message)
- {
- // Notify direct listeners
- _notify(channel, message);
- // Notify the globbing listeners
- var channelParts = channel.split("/");
- var last = channelParts.length - 1;
- for (var i = last; i > 0; --i)
- {
- var channelPart = channelParts.slice(0, i).join('/') + '/*';
- // We don't want to notify /foo/* if the channel is /foo/bar/baz,
- // so we stop at the first non recursive globbing
- if (i == last) _notify(channelPart, message);
- // Add the recursive globber and notify
- channelPart += '*';
- _notify(channelPart, message);
- }
- };
- function _notify(channel, message)
- {
- var subscriptions = _listeners[channel];
- if (subscriptions && subscriptions.length > 0)
- {
- for (var i = 0; i < subscriptions.length; ++i)
- {
- var subscription = subscriptions[i];
- // Subscriptions may come and go, so the array may have 'holes'
- if (subscription)
- {
- try
- {
- _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
- subscription.callback.call(subscription.scope, message);
- }
- catch (x)
- {
- // Ignore exceptions from callbacks
- _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x);
- }
- }
- }
- }
- };
- function _resetBackoff()
- {
- _backoff = 0;
- };
- function _increaseBackoff()
- {
- if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
- };
- var _error = this._error = function(text, args)
- {
- _log('error', _format.apply(this, arguments));
- };
- var _warn = this._warn = function(text, args)
- {
- _log('warn', _format.apply(this, arguments));
- };
- var _info = this._info = function(text, args)
- {
- _log('info', _format.apply(this, arguments));
- };
- var _debug = this._debug = function(text, args)
- {
- _log('debug', _format.apply(this, arguments));
- };
- function _log(level, text)
- {
- var priority = _logPriorities[level];
- var configPriority = _logPriorities[_logLevel];
- if (!configPriority) configPriority = _logPriorities['info'];
- if (priority >= configPriority)
- {
- if (window.console) window.console.log(text);
- }
- };
- function _format(text)
- {
- var braces = /\{\}/g;
- var result = '';
- var start = 0;
- var count = 0;
- while (braces.test(text))
- {
- result += text.substr(start, braces.lastIndex - start - 2);
- var arg = arguments[++count];
- result += arg !== undefined ? arg : '{}';
- start = braces.lastIndex;
- }
- result += text.substr(start, text.length - start);
- return result;
- };
- function newLongPollingTransport()
- {
- return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
- };
- function newCallbackPollingTransport()
- {
- return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
- };
- /**
- * Base object with the common functionality for transports.
- * The key responsibility is to allow at most 2 outstanding requests to the server,
- * to avoid that requests are sent behind a long poll.
- * To achieve this, we have one reserved request for the long poll, and all other
- * requests are serialized one after the other.
- */
- var Transport = function(type)
- {
- var _maxRequests = 2;
- var _requestIds = 0;
- var _cometRequest = null;
- var _requests = [];
- var _packets = [];
- this.getType = function()
- {
- return type;
- };
- this.send = function(packet, comet)
- {
- if (comet)
- _cometSend(this, packet);
- else
- _send(this, packet);
- };
- function _cometSend(self, packet)
- {
- if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
- var requestId = ++_requestIds;
- _debug('Beginning comet request {}', requestId);
- var request = {id: requestId};
- _debug('Delivering comet request {}', requestId);
- self.deliver(packet, request);
- _cometRequest = request;
- };
- function _send(self, packet)
- {
- var requestId = ++_requestIds;
- _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
- var request = {id: requestId};
- // Consider the comet request which should always be present
- if (_requests.length < _maxRequests - 1)
- {
- _debug('Delivering request {}', requestId);
- self.deliver(packet, request);
- _requests.push(request);
- }
- else
- {
- _packets.push([packet, request]);
- _debug('Queued request {}, {} queued requests', requestId, _packets.length);
- }
- };
- this.complete = function(request, success, comet)
- {
- if (comet)
- _cometComplete(request);
- else
- _complete(this, request, success);
- };
- function _cometComplete(request)
- {
- var requestId = request.id;
- if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
- // Reset comet request
- _cometRequest = null;
- _debug('Ended comet request {}', requestId);
- };
- function _complete(self, request, success)
- {
- var requestId = request.id;
- var index = $.inArray(request, _requests);
- // The index can be negative the request has been aborted
- if (index >= 0) _requests.splice(index, 1);
- _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
- if (_packets.length > 0)
- {
- var packet = _packets.shift();
- if (success)
- {
- _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
- _send(self, packet[0]);
- }
- else
- {
- _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length);
- // Keep the semantic of calling response callbacks asynchronously after the request
- setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0);
- }
- }
- };
- this.abort = function()
- {
- for (var i = 0; i < _requests.length; ++i)
- {
- var request = _requests[i];
- _debug('Aborting request {}', request.id);
- if (request.xhr) request.xhr.abort();
- }
- if (_cometRequest)
- {
- _debug('Aborting comet request {}', _cometRequest.id);
- if (_cometRequest.xhr) _cometRequest.xhr.abort();
- }
- _cometRequest = null;
- _requests = [];
- _packets = [];
- };
- };
- var LongPollingTransport = function()
- {
- this.deliver = function(packet, request)
- {
- request.xhr = $.ajax({
- url: packet.url,
- type: 'POST',
- contentType: 'text/json;charset=UTF-8',
- beforeSend: function(xhr)
- {
- xhr.setRequestHeader('Connection', 'Keep-Alive');
- return true;
- },
- data: JSON.stringify(packet.messages),
- success: function(response) { packet.onSuccess(request, response); },
- error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
- });
- };
- };
- var CallbackPollingTransport = function()
- {
- var _maxLength = 2000;
- this.deliver = function(packet, request)
- {
- // Microsoft Internet Explorer has a 2083 URL max length
- // We must ensure that we stay within that length
- var messages = JSON.stringify(packet.messages);
- // Encode the messages because all brackets, quotes, commas, colons, etc
- // present in the JSON will be URL encoded, taking many more characters
- var urlLength = packet.url.length + encodeURI(messages).length;
- _debug('URL length: {}', urlLength);
- // Let's stay on the safe side and use 2000 instead of 2083
- // also because we did not count few characters among which
- // the parameter name 'message' and the parameter 'jsonp',
- // which sum up to about 50 chars
- if (urlLength > _maxLength)
- {
- var x = packet.messages.length > 1 ?
- 'Too many bayeux messages in the same batch resulting in message too big ' +
- '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() :
- 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
- 'for transport ' + this.getType();
- // Keep the semantic of calling response callbacks asynchronously after the request
- _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0);
- }
- else
- {
- $.ajax({
- url: packet.url,
- type: 'GET',
- dataType: 'jsonp',
- jsonp: 'jsonp',
- beforeSend: function(xhr)
- {
- xhr.setRequestHeader('Connection', 'Keep-Alive');
- return true;
- },
- data:
- {
- // In callback-polling, the content must be sent via the 'message' parameter
- message: messages
- },
- success: function(response) { packet.onSuccess(request, response); },
- error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
- });
- }
- };
- };
- };
- /**
- * The JS object that exposes the comet API to applications
- */
- $.cometd = new $.Cometd(); // The default instance
- })(jQuery);
|