jquery.comet.js 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452
  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. * Returns the name assigned to this Comet object, or the string 'default'
  74. * if no name has been explicitely passed as parameter to the constructor.
  75. */
  76. this.getName = function()
  77. {
  78. return _name;
  79. };
  80. /**
  81. * Configures the initial comet communication with the comet server.
  82. * @param cometURL the URL of the comet server
  83. */
  84. this.configure = function(cometURL)
  85. {
  86. _configure(cometURL);
  87. };
  88. function _configure(cometURL)
  89. {
  90. _url = cometURL;
  91. _debug('Initializing comet with url: {}', _url);
  92. // Check immediately if we're cross domain
  93. // If cross domain, the handshake must not send the long polling transport type
  94. var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL);
  95. if (urlParts[3]) _xd = urlParts[3] != location.host;
  96. // Temporary setup a transport to send the initial handshake
  97. // The transport may be changed as a result of handshake
  98. if (_xd)
  99. _transport = newCallbackPollingTransport();
  100. else
  101. _transport = newLongPollingTransport();
  102. _debug('Initial transport is {}', _transport.getType());
  103. };
  104. /**
  105. * Configures and establishes the comet communication with the comet server
  106. * via a handshake and a subsequent connect.
  107. * @param cometURL the URL of the comet server
  108. * @param handshakeProps an object to be merged with the handshake message
  109. * @see #configure(cometURL)
  110. * @see #handshake(handshakeProps)
  111. */
  112. this.init = function(cometURL, handshakeProps)
  113. {
  114. _configure(cometURL);
  115. _handshake(handshakeProps);
  116. };
  117. /**
  118. * Establishes the comet communication with the comet server
  119. * via a handshake and a subsequent connect.
  120. * @param handshakeProps an object to be merged with the handshake message
  121. */
  122. this.handshake = function(handshakeProps)
  123. {
  124. _handshake(handshakeProps);
  125. };
  126. /**
  127. * Disconnects from the comet server.
  128. * @param disconnectProps an object to be merged with the disconnect message
  129. */
  130. this.disconnect = function(disconnectProps)
  131. {
  132. var bayeuxMessage = {
  133. channel: '/meta/disconnect'
  134. };
  135. var message = $.extend({}, disconnectProps, bayeuxMessage);
  136. // Deliver immediately
  137. // The handshake and connect mechanism make use of startBatch(), and in case
  138. // of a failed handshake the disconnect would not be delivered if using _send().
  139. _setStatus('disconnecting');
  140. _deliver([message], false);
  141. };
  142. /**
  143. * Marks the start of a batch of application messages to be sent to the server
  144. * in a single request, obtaining a single response containing (possibly) many
  145. * application reply messages.
  146. * Messages are held in a queue and not sent until {@link #endBatch()} is called.
  147. * If startBatch() is called multiple times, then an equal number of endBatch()
  148. * calls must be made to close and send the batch of messages.
  149. * @see #endBatch()
  150. */
  151. this.startBatch = function()
  152. {
  153. _startBatch();
  154. };
  155. /**
  156. * Marks the end of a batch of application messages to be sent to the server
  157. * in a single request.
  158. * @see #startBatch()
  159. */
  160. this.endBatch = function()
  161. {
  162. _endBatch(true);
  163. };
  164. /**
  165. * Subscribes to the given channel, performing the given callback in the given scope
  166. * when a message for the channel arrives.
  167. * @param channel the channel to subscribe to
  168. * @param scope the scope of the callback
  169. * @param callback the callback to call when a message is delivered to the channel
  170. * @param subscribeProps an object to be merged with the subscribe message
  171. * @return the subscription handle to be passed to {@link #unsubscribe(object)}
  172. */
  173. this.subscribe = function(channel, scope, callback, subscribeProps)
  174. {
  175. var subscription = this.addListener(channel, scope, callback);
  176. // Send the subscription message after the subscription registration to avoid
  177. // races where the server would deliver a message to the subscribers, but here
  178. // on the client the subscription has not been added yet to the data structures
  179. var bayeuxMessage = {
  180. channel: '/meta/subscribe',
  181. subscription: channel
  182. };
  183. var message = $.extend({}, subscribeProps, bayeuxMessage);
  184. _send(message);
  185. return subscription;
  186. };
  187. /**
  188. * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
  189. * @param subscription the subscription to unsubscribe.
  190. */
  191. this.unsubscribe = function(subscription, unsubscribeProps)
  192. {
  193. // Remove the local listener before sending the message
  194. // This ensures that if the server fails, this client does not get notifications
  195. this.removeListener(subscription);
  196. var bayeuxMessage = {
  197. channel: '/meta/unsubscribe',
  198. subscription: subscription[0]
  199. };
  200. var message = $.extend({}, unsubscribeProps, bayeuxMessage);
  201. _send(message);
  202. };
  203. /**
  204. * Publishes a message on the given channel, containing the given content.
  205. * @param channel the channel to publish the message to
  206. * @param content the content of the message
  207. * @param publishProps an object to be merged with the publish message
  208. */
  209. this.publish = function(channel, content, publishProps)
  210. {
  211. var bayeuxMessage = {
  212. channel: channel,
  213. data: content
  214. };
  215. var message = $.extend({}, publishProps, bayeuxMessage);
  216. _send(message);
  217. };
  218. /**
  219. * Adds a listener for bayeux messages, performing the given callback in the given scope
  220. * when a message for the given channel arrives.
  221. * @param channel the channel the listener is interested to
  222. * @param scope the scope of the callback
  223. * @param callback the callback to call when a message is delivered to the channel
  224. * @returns the subscription handle to be passed to {@link #removeListener(object)}
  225. * @see #removeListener(object)
  226. */
  227. this.addListener = function(channel, scope, callback)
  228. {
  229. // The data structure is a map<channel, subscription[]>, where each subscription
  230. // holds the callback to be called and its scope.
  231. // Normalize arguments
  232. if (!callback)
  233. {
  234. callback = scope;
  235. scope = undefined;
  236. }
  237. var subscription = {
  238. scope: scope,
  239. callback: callback
  240. };
  241. var subscriptions = _listeners[channel];
  242. if (!subscriptions)
  243. {
  244. subscriptions = [];
  245. _listeners[channel] = subscriptions;
  246. }
  247. // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
  248. // Note that if:
  249. // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
  250. // then:
  251. // hc==3, a.join()=='a',,'c', a.length==3
  252. var subscriptionIndex = subscriptions.push(subscription) - 1;
  253. _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex);
  254. // The subscription to allow removal of the listener is made of the channel and the index
  255. return [channel, subscriptionIndex];
  256. };
  257. /**
  258. * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
  259. * @param subscription the subscription to unsubscribe.
  260. */
  261. this.removeListener = function(subscription)
  262. {
  263. var subscriptions = _listeners[subscription[0]];
  264. if (subscriptions)
  265. {
  266. delete subscriptions[subscription[1]];
  267. _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
  268. }
  269. };
  270. /**
  271. * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
  272. * {@link #subscribe(channel, scope, callback)}.
  273. */
  274. this.clearListeners = function()
  275. {
  276. _listeners = {};
  277. };
  278. /**
  279. * Returns a string representing the status of the bayeux communication with the comet server.
  280. */
  281. this.getStatus = function()
  282. {
  283. return _status;
  284. };
  285. /**
  286. * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
  287. * Default value is 1 second, which means if there is a persistent failure the retries will happen
  288. * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
  289. * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
  290. * @param period the backoff period to set
  291. * @see #getBackoffIncrement()
  292. */
  293. this.setBackoffIncrement = function(period)
  294. {
  295. _backoffIncrement = period;
  296. };
  297. /**
  298. * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
  299. * @see #setBackoffIncrement(period)
  300. */
  301. this.getBackoffIncrement = function()
  302. {
  303. return _backoffIncrement;
  304. };
  305. /**
  306. * Returns the backoff period to wait before retrying an unsuccessful or failed message.
  307. */
  308. this.getBackoffPeriod = function()
  309. {
  310. return _backoff;
  311. };
  312. /**
  313. * Sets the log level for console logging.
  314. * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
  315. * less verbose to more verbose.
  316. * @param level the log level string
  317. */
  318. this.setLogLevel = function(level)
  319. {
  320. _logLevel = level;
  321. };
  322. /**
  323. * Registers an extension whose callbacks are called for every incoming message
  324. * (that comes from the server to this client implementation) and for every
  325. * outgoing message (that originates from this client implementation for the
  326. * server).
  327. * The format of the extension object is the following:
  328. * <pre>
  329. * {
  330. * incoming: function(message) { ... },
  331. * outgoing: function(message) { ... }
  332. * }
  333. * Both properties are optional, but if they are present they will be called
  334. * respectively for each incoming message and for each outgoing message.
  335. * </pre>
  336. * @param name the name of the extension
  337. * @param extension the extension to register
  338. * @return true if the extension was registered, false otherwise
  339. * @see #unregisterExtension(name)
  340. */
  341. this.registerExtension = function(name, extension)
  342. {
  343. var existing = false;
  344. for (var i = 0; i < _extensions.length; ++i)
  345. {
  346. var existingExtension = _extensions[i];
  347. if (existingExtension.name == name)
  348. {
  349. existing = true;
  350. return false;
  351. }
  352. }
  353. if (!existing)
  354. {
  355. _extensions.push({
  356. name: name,
  357. extension: extension
  358. });
  359. _debug('Registered extension \'{}\'', name);
  360. return true;
  361. }
  362. else
  363. {
  364. _info('Could not register extension with name \'{}\': another extension with the same name already exists');
  365. return false;
  366. }
  367. };
  368. /**
  369. * Unregister an extension previously registered with
  370. * {@link #registerExtension(name, extension)}.
  371. * @param name the name of the extension to unregister.
  372. * @return true if the extension was unregistered, false otherwise
  373. */
  374. this.unregisterExtension = function(name)
  375. {
  376. var unregistered = false;
  377. $.each(_extensions, function(index, extension)
  378. {
  379. if (extension.name == name)
  380. {
  381. _extensions.splice(index, 1);
  382. unregistered = true;
  383. _debug('Unregistered extension \'{}\'', name);
  384. return false;
  385. }
  386. });
  387. return unregistered;
  388. };
  389. /**
  390. * Starts a the batch of messages to be sent in a single request.
  391. * @see _endBatch(deliverMessages)
  392. */
  393. function _startBatch()
  394. {
  395. ++_batch;
  396. };
  397. /**
  398. * Ends the batch of messages to be sent in a single request,
  399. * optionally delivering messages present in the message queue depending
  400. * on the given argument.
  401. * @param deliverMessages whether to deliver the messages in the queue or not
  402. * @see _startBatch()
  403. */
  404. function _endBatch(deliverMessages)
  405. {
  406. --_batch;
  407. if (_batch < 0) _batch = 0;
  408. if (deliverMessages && _batch == 0 && !_isDisconnected())
  409. {
  410. var messages = _messageQueue;
  411. _messageQueue = [];
  412. if (messages.length > 0) _deliver(messages, false);
  413. }
  414. };
  415. function _nextMessageId()
  416. {
  417. return ++_messageId;
  418. };
  419. /**
  420. * Converts the given response into an array of bayeux messages
  421. * @param response the response to convert
  422. * @return an array of bayeux messages obtained by converting the response
  423. */
  424. function _convertToMessages(response)
  425. {
  426. if (response === undefined) return [];
  427. if (response instanceof Array) return response;
  428. if (response instanceof String || typeof response == 'string') return eval('(' + response + ')');
  429. if (response instanceof Object) return [response];
  430. throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
  431. };
  432. function _setStatus(newStatus)
  433. {
  434. _debug('{} -> {}', _status, newStatus);
  435. _status = newStatus;
  436. };
  437. function _isDisconnected()
  438. {
  439. return _status == 'disconnecting' || _status == 'disconnected';
  440. };
  441. /**
  442. * Sends the initial handshake message
  443. */
  444. function _handshake(handshakeProps)
  445. {
  446. _debug('Starting handshake');
  447. _clientId = null;
  448. // Start a batch.
  449. // This is needed because handshake and connect are async.
  450. // It may happen that the application calls init() then subscribe()
  451. // and the subscribe message is sent before the connect message, if
  452. // the subscribe message is not held until the connect message is sent.
  453. // So here we start a batch to hold temporarly any message until
  454. // the connection is fully established.
  455. _batch = 0;
  456. _startBatch();
  457. // Save the original properties provided by the user
  458. // Deep copy to avoid the user to be able to change them later
  459. _handshakeProps = $.extend(true, {}, handshakeProps);
  460. var bayeuxMessage = {
  461. version: '1.0',
  462. minimumVersion: '0.9',
  463. channel: '/meta/handshake',
  464. supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
  465. };
  466. // Do not allow the user to mess with the required properties,
  467. // so merge first the user properties and *then* the bayeux message
  468. var message = $.extend({}, handshakeProps, bayeuxMessage);
  469. // We started a batch to hold the application messages,
  470. // so here we must bypass it and deliver immediately.
  471. _setStatus('handshaking');
  472. _deliver([message], false);
  473. };
  474. function _findTransport(handshakeResponse)
  475. {
  476. var transportTypes = handshakeResponse.supportedConnectionTypes;
  477. if (_xd)
  478. {
  479. // If we are cross domain, check if the server supports it, that's the only option
  480. if ($.inArray('callback-polling', transportTypes) >= 0) return _transport;
  481. }
  482. else
  483. {
  484. // Check if we can keep long-polling
  485. if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
  486. // The server does not support long-polling
  487. if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
  488. }
  489. return null;
  490. };
  491. function _delayedHandshake()
  492. {
  493. _setStatus('handshaking');
  494. _delayedSend(function()
  495. {
  496. _handshake(_handshakeProps);
  497. });
  498. };
  499. function _delayedConnect()
  500. {
  501. _setStatus('connecting');
  502. _delayedSend(function()
  503. {
  504. _connect();
  505. });
  506. };
  507. function _delayedSend(operation)
  508. {
  509. _cancelDelayedSend();
  510. var delay = _backoff;
  511. _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval);
  512. if (_advice.interval && _advice.interval > 0)
  513. delay += _advice.interval;
  514. _scheduledSend = _setTimeout(operation, delay);
  515. };
  516. function _cancelDelayedSend()
  517. {
  518. if (_scheduledSend !== null) clearTimeout(_scheduledSend);
  519. _scheduledSend = null;
  520. };
  521. function _setTimeout(funktion, delay)
  522. {
  523. return setTimeout(function()
  524. {
  525. try
  526. {
  527. funktion();
  528. }
  529. catch (x)
  530. {
  531. _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
  532. }
  533. }, delay);
  534. };
  535. /**
  536. * Sends the connect message
  537. */
  538. function _connect()
  539. {
  540. _debug('Starting connect');
  541. var message = {
  542. channel: '/meta/connect',
  543. connectionType: _transport.getType()
  544. };
  545. _setStatus('connecting');
  546. _deliver([message], true);
  547. _setStatus('connected');
  548. };
  549. function _send(message)
  550. {
  551. if (_batch > 0)
  552. _messageQueue.push(message);
  553. else
  554. _deliver([message], false);
  555. };
  556. /**
  557. * Delivers the messages to the comet server
  558. * @param messages the array of messages to send
  559. */
  560. function _deliver(messages, comet)
  561. {
  562. // We must be sure that the messages have a clientId.
  563. // This is not guaranteed since the handshake may take time to return
  564. // (and hence the clientId is not known yet) and the application
  565. // may create other messages.
  566. $.each(messages, function(index, message)
  567. {
  568. message['id'] = _nextMessageId();
  569. if (_clientId) message['clientId'] = _clientId;
  570. messages[index] = _applyOutgoingExtensions(message);
  571. });
  572. var self = this;
  573. var envelope = {
  574. url: _url,
  575. messages: messages,
  576. onSuccess: function(request, response)
  577. {
  578. try
  579. {
  580. _handleSuccess.call(self, request, response, comet);
  581. }
  582. catch (x)
  583. {
  584. _debug('Exception during execution of success callback: {}', x);
  585. }
  586. },
  587. onFailure: function(request, reason, exception)
  588. {
  589. try
  590. {
  591. _handleFailure.call(self, request, messages, reason, exception, comet);
  592. }
  593. catch (x)
  594. {
  595. _debug('Exception during execution of failure callback: {}', x);
  596. }
  597. }
  598. };
  599. _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
  600. _transport.send(envelope, comet);
  601. };
  602. function _applyIncomingExtensions(message)
  603. {
  604. for (var i = 0; i < _extensions.length; ++i)
  605. {
  606. var extension = _extensions[i];
  607. var callback = extension.extension.incoming;
  608. if (callback && typeof callback === 'function')
  609. {
  610. _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
  611. message = _applyExtension(extension.name, callback, message) || message;
  612. }
  613. }
  614. return message;
  615. };
  616. function _applyOutgoingExtensions(message)
  617. {
  618. for (var i = 0; i < _extensions.length; ++i)
  619. {
  620. var extension = _extensions[i];
  621. var callback = extension.extension.outgoing;
  622. if (callback && typeof callback === 'function')
  623. {
  624. _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
  625. message = _applyExtension(extension.name, callback, message) || message;
  626. }
  627. }
  628. return message;
  629. };
  630. function _applyExtension(name, callback, message)
  631. {
  632. try
  633. {
  634. return callback(message);
  635. }
  636. catch (x)
  637. {
  638. _debug('Exception during execution of extension \'{}\': {}', name, x);
  639. return message;
  640. }
  641. };
  642. function _handleSuccess(request, response, comet)
  643. {
  644. var messages = _convertToMessages(response);
  645. _debug('Received response {}', JSON.stringify(messages));
  646. // Signal the transport it can deliver other queued requests
  647. _transport.complete(request, true, comet);
  648. for (var i = 0; i < messages.length; ++i)
  649. {
  650. var message = messages[i];
  651. message = _applyIncomingExtensions(message);
  652. if (message.advice) _advice = message.advice;
  653. var channel = message.channel;
  654. switch (channel)
  655. {
  656. case '/meta/handshake':
  657. _handshakeSuccess(message);
  658. break;
  659. case '/meta/connect':
  660. _connectSuccess(message);
  661. break;
  662. case '/meta/disconnect':
  663. _disconnectSuccess(message);
  664. break;
  665. case '/meta/subscribe':
  666. _subscribeSuccess(message);
  667. break;
  668. case '/meta/unsubscribe':
  669. _unsubscribeSuccess(message);
  670. break;
  671. default:
  672. _messageSuccess(message);
  673. break;
  674. }
  675. }
  676. };
  677. function _handleFailure(request, messages, reason, exception, comet)
  678. {
  679. var xhr = request.xhr;
  680. _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
  681. // Signal the transport it can deliver other queued requests
  682. _transport.complete(request, false, comet);
  683. for (var i = 0; i < messages.length; ++i)
  684. {
  685. var message = messages[i];
  686. var channel = message.channel;
  687. switch (channel)
  688. {
  689. case '/meta/handshake':
  690. _handshakeFailure(xhr, message);
  691. break;
  692. case '/meta/connect':
  693. _connectFailure(xhr, message);
  694. break;
  695. case '/meta/disconnect':
  696. _disconnectFailure(xhr, message);
  697. break;
  698. case '/meta/subscribe':
  699. _subscribeFailure(xhr, message);
  700. break;
  701. case '/meta/unsubscribe':
  702. _unsubscribeFailure(xhr, message);
  703. break;
  704. default:
  705. _messageFailure(xhr, message);
  706. break;
  707. }
  708. }
  709. };
  710. function _handshakeSuccess(message)
  711. {
  712. if (message.successful)
  713. {
  714. _debug('Handshake successful');
  715. // Save clientId, figure out transport, then follow the advice to connect
  716. _clientId = message.clientId;
  717. var newTransport = _findTransport(message);
  718. if (newTransport === null)
  719. {
  720. throw 'Could not agree on transport with server';
  721. }
  722. else
  723. {
  724. if (_transport.getType() != newTransport.getType())
  725. {
  726. _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
  727. _transport = newTransport;
  728. }
  729. }
  730. // Notify the listeners
  731. // Here the new transport is in place, as well as the clientId, so
  732. // the listener can perform a publish() if it wants, and the listeners
  733. // are notified before the connect below.
  734. _notifyListeners('/meta/handshake', message);
  735. var action = _advice.reconnect ? _advice.reconnect : 'retry';
  736. switch (action)
  737. {
  738. case 'retry':
  739. _delayedConnect();
  740. break;
  741. default:
  742. break;
  743. }
  744. }
  745. else
  746. {
  747. _debug('Handshake unsuccessful');
  748. var retry = !_isDisconnected() && _advice.reconnect != 'none';
  749. if (!retry) _setStatus('disconnected');
  750. _notifyListeners('/meta/handshake', message);
  751. _notifyListeners('/meta/unsuccessful', message);
  752. // Only try again if we haven't been disconnected and
  753. // the advice permits us to retry the handshake
  754. if (retry)
  755. {
  756. _increaseBackoff();
  757. _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
  758. _delayedHandshake();
  759. }
  760. }
  761. };
  762. function _handshakeFailure(xhr, message)
  763. {
  764. _debug('Handshake failure');
  765. // Notify listeners
  766. var failureMessage = {
  767. successful: false,
  768. failure: true,
  769. channel: '/meta/handshake',
  770. request: message,
  771. xhr: xhr,
  772. advice: {
  773. action: 'retry',
  774. interval: _backoff
  775. }
  776. };
  777. var retry = !_isDisconnected() && _advice.reconnect != 'none';
  778. if (!retry) _setStatus('disconnected');
  779. _notifyListeners('/meta/handshake', failureMessage);
  780. _notifyListeners('/meta/unsuccessful', failureMessage);
  781. // Only try again if we haven't been disconnected and the
  782. // advice permits us to try again
  783. if (retry)
  784. {
  785. _increaseBackoff();
  786. _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
  787. _delayedHandshake();
  788. }
  789. };
  790. function _connectSuccess(message)
  791. {
  792. var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
  793. if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
  794. if (message.successful)
  795. {
  796. _debug('Connect successful');
  797. // End the batch and allow held messages from the application
  798. // to go to the server (see _handshake() where we start the batch).
  799. // The batch is ended before notifying the listeners, so that
  800. // listeners can batch other cometd operations
  801. _endBatch(true);
  802. // Notify the listeners after the status change but before the next connect
  803. _notifyListeners('/meta/connect', message);
  804. // Connect was successful.
  805. // Normally, the advice will say "reconnect: 'retry', interval: 0"
  806. // and the server will hold the request, so when a response returns
  807. // we immediately call the server again (long polling)
  808. switch (action)
  809. {
  810. case 'retry':
  811. _resetBackoff();
  812. _delayedConnect();
  813. break;
  814. default:
  815. _resetBackoff();
  816. _setStatus('disconnected');
  817. break;
  818. }
  819. }
  820. else
  821. {
  822. _debug('Connect unsuccessful');
  823. // Notify the listeners after the status change but before the next action
  824. _notifyListeners('/meta/connect', message);
  825. _notifyListeners('/meta/unsuccessful', message);
  826. // Connect was not successful.
  827. // This may happen when the server crashed, the current clientId
  828. // will be invalid, and the server will ask to handshake again
  829. switch (action)
  830. {
  831. case 'retry':
  832. _increaseBackoff();
  833. _delayedConnect();
  834. break;
  835. case 'handshake':
  836. // End the batch but do not deliver the messages until we connect successfully
  837. _endBatch(false);
  838. _resetBackoff();
  839. _delayedHandshake();
  840. break;
  841. case 'none':
  842. _resetBackoff();
  843. _setStatus('disconnected');
  844. break;
  845. }
  846. }
  847. };
  848. function _connectFailure(xhr, message)
  849. {
  850. _debug('Connect failure');
  851. // Notify listeners
  852. var failureMessage = {
  853. successful: false,
  854. failure: true,
  855. channel: '/meta/connect',
  856. request: message,
  857. xhr: xhr,
  858. advice: {
  859. action: 'retry',
  860. interval: _backoff
  861. }
  862. };
  863. _notifyListeners('/meta/connect', failureMessage);
  864. _notifyListeners('/meta/unsuccessful', failureMessage);
  865. if (!_isDisconnected())
  866. {
  867. var action = _advice.reconnect ? _advice.reconnect : 'retry';
  868. switch (action)
  869. {
  870. case 'retry':
  871. _increaseBackoff();
  872. _debug('Connect failure, backing off and retrying in {} ms', _backoff);
  873. _delayedConnect();
  874. break;
  875. case 'handshake':
  876. _resetBackoff();
  877. _delayedHandshake();
  878. break;
  879. case 'none':
  880. _resetBackoff();
  881. break;
  882. default:
  883. _debug('Unrecognized reconnect value: {}', action);
  884. break;
  885. }
  886. }
  887. };
  888. function _disconnectSuccess(message)
  889. {
  890. if (message.successful)
  891. {
  892. _debug('Disconnect successful');
  893. _disconnect(false);
  894. _notifyListeners('/meta/disconnect', message);
  895. }
  896. else
  897. {
  898. _debug('Disconnect unsuccessful');
  899. _disconnect(true);
  900. _notifyListeners('/meta/disconnect', message);
  901. _notifyListeners('/meta/usuccessful', message);
  902. }
  903. };
  904. function _disconnect(abort)
  905. {
  906. _cancelDelayedSend();
  907. if (abort) _transport.abort();
  908. _clientId = null;
  909. _setStatus('disconnected');
  910. _batch = 0;
  911. _messageQueue = [];
  912. _resetBackoff();
  913. };
  914. function _disconnectFailure(xhr, message)
  915. {
  916. _debug('Disconnect failure');
  917. _disconnect(true);
  918. var failureMessage = {
  919. successful: false,
  920. failure: true,
  921. channel: '/meta/disconnect',
  922. request: message,
  923. xhr: xhr,
  924. advice: {
  925. action: 'none',
  926. interval: 0
  927. }
  928. };
  929. _notifyListeners('/meta/disconnect', failureMessage);
  930. _notifyListeners('/meta/unsuccessful', failureMessage);
  931. };
  932. function _subscribeSuccess(message)
  933. {
  934. if (message.successful)
  935. {
  936. _debug('Subscribe successful');
  937. _notifyListeners('/meta/subscribe', message);
  938. }
  939. else
  940. {
  941. _debug('Subscribe unsuccessful');
  942. _notifyListeners('/meta/subscribe', message);
  943. _notifyListeners('/meta/unsuccessful', message);
  944. }
  945. };
  946. function _subscribeFailure(xhr, message)
  947. {
  948. _debug('Subscribe failure');
  949. var failureMessage = {
  950. successful: false,
  951. failure: true,
  952. channel: '/meta/subscribe',
  953. request: message,
  954. xhr: xhr,
  955. advice: {
  956. action: 'none',
  957. interval: 0
  958. }
  959. };
  960. _notifyListeners('/meta/subscribe', failureMessage);
  961. _notifyListeners('/meta/unsuccessful', failureMessage);
  962. };
  963. function _unsubscribeSuccess(message)
  964. {
  965. if (message.successful)
  966. {
  967. _debug('Unsubscribe successful');
  968. _notifyListeners('/meta/unsubscribe', message);
  969. }
  970. else
  971. {
  972. _debug('Unsubscribe unsuccessful');
  973. _notifyListeners('/meta/unsubscribe', message);
  974. _notifyListeners('/meta/unsuccessful', message);
  975. }
  976. };
  977. function _unsubscribeFailure(xhr, message)
  978. {
  979. _debug('Unsubscribe failure');
  980. var failureMessage = {
  981. successful: false,
  982. failure: true,
  983. channel: '/meta/unsubscribe',
  984. request: message,
  985. xhr: xhr,
  986. advice: {
  987. action: 'none',
  988. interval: 0
  989. }
  990. };
  991. _notifyListeners('/meta/unsubscribe', failureMessage);
  992. _notifyListeners('/meta/unsuccessful', failureMessage);
  993. };
  994. function _messageSuccess(message)
  995. {
  996. if (message.successful === undefined)
  997. {
  998. if (message.data)
  999. {
  1000. // It is a plain message, and not a bayeux meta message
  1001. _notifyListeners(message.channel, message);
  1002. }
  1003. else
  1004. {
  1005. _debug('Unknown message {}', JSON.stringify(message));
  1006. }
  1007. }
  1008. else
  1009. {
  1010. if (message.successful)
  1011. {
  1012. _debug('Publish successful');
  1013. _notifyListeners('/meta/publish', message);
  1014. }
  1015. else
  1016. {
  1017. _debug('Publish unsuccessful');
  1018. _notifyListeners('/meta/publish', message);
  1019. _notifyListeners('/meta/unsuccessful', message);
  1020. }
  1021. }
  1022. };
  1023. function _messageFailure(xhr, message)
  1024. {
  1025. _debug('Publish failure');
  1026. var failureMessage = {
  1027. successful: false,
  1028. failure: true,
  1029. channel: message.channel,
  1030. request: message,
  1031. xhr: xhr,
  1032. advice: {
  1033. action: 'none',
  1034. interval: 0
  1035. }
  1036. };
  1037. _notifyListeners('/meta/publish', failureMessage);
  1038. _notifyListeners('/meta/unsuccessful', failureMessage);
  1039. };
  1040. function _notifyListeners(channel, message)
  1041. {
  1042. // Notify direct listeners
  1043. _notify(channel, message);
  1044. // Notify the globbing listeners
  1045. var channelParts = channel.split("/");
  1046. var last = channelParts.length - 1;
  1047. for (var i = last; i > 0; --i)
  1048. {
  1049. var channelPart = channelParts.slice(0, i).join('/') + '/*';
  1050. // We don't want to notify /foo/* if the channel is /foo/bar/baz,
  1051. // so we stop at the first non recursive globbing
  1052. if (i == last) _notify(channelPart, message);
  1053. // Add the recursive globber and notify
  1054. channelPart += '*';
  1055. _notify(channelPart, message);
  1056. }
  1057. };
  1058. function _notify(channel, message)
  1059. {
  1060. var subscriptions = _listeners[channel];
  1061. if (subscriptions && subscriptions.length > 0)
  1062. {
  1063. for (var i = 0; i < subscriptions.length; ++i)
  1064. {
  1065. var subscription = subscriptions[i];
  1066. // Subscriptions may come and go, so the array may have 'holes'
  1067. if (subscription)
  1068. {
  1069. try
  1070. {
  1071. _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
  1072. subscription.callback.call(subscription.scope, message);
  1073. }
  1074. catch (x)
  1075. {
  1076. // Ignore exceptions from callbacks
  1077. _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x);
  1078. }
  1079. }
  1080. }
  1081. }
  1082. };
  1083. function _resetBackoff()
  1084. {
  1085. _backoff = 0;
  1086. };
  1087. function _increaseBackoff()
  1088. {
  1089. if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
  1090. };
  1091. var _error = this._error = function(text, args)
  1092. {
  1093. _log('error', _format.apply(this, arguments));
  1094. };
  1095. var _warn = this._warn = function(text, args)
  1096. {
  1097. _log('warn', _format.apply(this, arguments));
  1098. };
  1099. var _info = this._info = function(text, args)
  1100. {
  1101. _log('info', _format.apply(this, arguments));
  1102. };
  1103. var _debug = this._debug = function(text, args)
  1104. {
  1105. _log('debug', _format.apply(this, arguments));
  1106. };
  1107. function _log(level, text)
  1108. {
  1109. var priority = _logPriorities[level];
  1110. var configPriority = _logPriorities[_logLevel];
  1111. if (!configPriority) configPriority = _logPriorities['info'];
  1112. if (priority >= configPriority)
  1113. {
  1114. if (window.console) window.console.log(text);
  1115. }
  1116. };
  1117. function _format(text)
  1118. {
  1119. var braces = /\{\}/g;
  1120. var result = '';
  1121. var start = 0;
  1122. var count = 0;
  1123. while (braces.test(text))
  1124. {
  1125. result += text.substr(start, braces.lastIndex - start - 2);
  1126. var arg = arguments[++count];
  1127. result += arg !== undefined ? arg : '{}';
  1128. start = braces.lastIndex;
  1129. }
  1130. result += text.substr(start, text.length - start);
  1131. return result;
  1132. };
  1133. function newLongPollingTransport()
  1134. {
  1135. return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
  1136. };
  1137. function newCallbackPollingTransport()
  1138. {
  1139. return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
  1140. };
  1141. /**
  1142. * Base object with the common functionality for transports.
  1143. * The key responsibility is to allow at most 2 outstanding requests to the server,
  1144. * to avoid that requests are sent behind a long poll.
  1145. * To achieve this, we have one reserved request for the long poll, and all other
  1146. * requests are serialized one after the other.
  1147. */
  1148. var Transport = function(type)
  1149. {
  1150. var _maxRequests = 2;
  1151. var _requestIds = 0;
  1152. var _cometRequest = null;
  1153. var _requests = [];
  1154. var _packets = [];
  1155. this.getType = function()
  1156. {
  1157. return type;
  1158. };
  1159. this.send = function(packet, comet)
  1160. {
  1161. if (comet)
  1162. _cometSend(this, packet);
  1163. else
  1164. _send(this, packet);
  1165. };
  1166. function _cometSend(self, packet)
  1167. {
  1168. if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
  1169. var requestId = ++_requestIds;
  1170. _debug('Beginning comet request {}', requestId);
  1171. var request = {id: requestId};
  1172. _debug('Delivering comet request {}', requestId);
  1173. self.deliver(packet, request);
  1174. _cometRequest = request;
  1175. };
  1176. function _send(self, packet)
  1177. {
  1178. var requestId = ++_requestIds;
  1179. _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
  1180. var request = {id: requestId};
  1181. // Consider the comet request which should always be present
  1182. if (_requests.length < _maxRequests - 1)
  1183. {
  1184. _debug('Delivering request {}', requestId);
  1185. self.deliver(packet, request);
  1186. _requests.push(request);
  1187. }
  1188. else
  1189. {
  1190. _packets.push([packet, request]);
  1191. _debug('Queued request {}, {} queued requests', requestId, _packets.length);
  1192. }
  1193. };
  1194. this.complete = function(request, success, comet)
  1195. {
  1196. if (comet)
  1197. _cometComplete(request);
  1198. else
  1199. _complete(this, request, success);
  1200. };
  1201. function _cometComplete(request)
  1202. {
  1203. var requestId = request.id;
  1204. if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
  1205. // Reset comet request
  1206. _cometRequest = null;
  1207. _debug('Ended comet request {}', requestId);
  1208. };
  1209. function _complete(self, request, success)
  1210. {
  1211. var requestId = request.id;
  1212. var index = $.inArray(request, _requests);
  1213. // The index can be negative the request has been aborted
  1214. if (index >= 0) _requests.splice(index, 1);
  1215. _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
  1216. if (_packets.length > 0)
  1217. {
  1218. var packet = _packets.shift();
  1219. if (success)
  1220. {
  1221. _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
  1222. _send(self, packet[0]);
  1223. }
  1224. else
  1225. {
  1226. _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length);
  1227. // Keep the semantic of calling response callbacks asynchronously after the request
  1228. setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0);
  1229. }
  1230. }
  1231. };
  1232. this.abort = function()
  1233. {
  1234. for (var i = 0; i < _requests.length; ++i)
  1235. {
  1236. var request = _requests[i];
  1237. _debug('Aborting request {}', request.id);
  1238. if (request.xhr) request.xhr.abort();
  1239. }
  1240. if (_cometRequest)
  1241. {
  1242. _debug('Aborting comet request {}', _cometRequest.id);
  1243. if (_cometRequest.xhr) _cometRequest.xhr.abort();
  1244. }
  1245. _cometRequest = null;
  1246. _requests = [];
  1247. _packets = [];
  1248. };
  1249. };
  1250. var LongPollingTransport = function()
  1251. {
  1252. this.deliver = function(packet, request)
  1253. {
  1254. request.xhr = $.ajax({
  1255. url: packet.url,
  1256. type: 'POST',
  1257. contentType: 'text/json;charset=UTF-8',
  1258. beforeSend: function(xhr)
  1259. {
  1260. xhr.setRequestHeader('Connection', 'Keep-Alive');
  1261. return true;
  1262. },
  1263. data: JSON.stringify(packet.messages),
  1264. success: function(response) { packet.onSuccess(request, response); },
  1265. error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
  1266. });
  1267. };
  1268. };
  1269. var CallbackPollingTransport = function()
  1270. {
  1271. var _maxLength = 2000;
  1272. this.deliver = function(packet, request)
  1273. {
  1274. // Microsoft Internet Explorer has a 2083 URL max length
  1275. // We must ensure that we stay within that length
  1276. var messages = JSON.stringify(packet.messages);
  1277. // Encode the messages because all brackets, quotes, commas, colons, etc
  1278. // present in the JSON will be URL encoded, taking many more characters
  1279. var urlLength = packet.url.length + encodeURI(messages).length;
  1280. _debug('URL length: {}', urlLength);
  1281. // Let's stay on the safe side and use 2000 instead of 2083
  1282. // also because we did not count few characters among which
  1283. // the parameter name 'message' and the parameter 'jsonp',
  1284. // which sum up to about 50 chars
  1285. if (urlLength > _maxLength)
  1286. {
  1287. var x = packet.messages.length > 1 ?
  1288. 'Too many bayeux messages in the same batch resulting in message too big ' +
  1289. '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() :
  1290. 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
  1291. 'for transport ' + this.getType();
  1292. // Keep the semantic of calling response callbacks asynchronously after the request
  1293. _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0);
  1294. }
  1295. else
  1296. {
  1297. $.ajax({
  1298. url: packet.url,
  1299. type: 'GET',
  1300. dataType: 'jsonp',
  1301. jsonp: 'jsonp',
  1302. beforeSend: function(xhr)
  1303. {
  1304. xhr.setRequestHeader('Connection', 'Keep-Alive');
  1305. return true;
  1306. },
  1307. data:
  1308. {
  1309. // In callback-polling, the content must be sent via the 'message' parameter
  1310. message: messages
  1311. },
  1312. success: function(response) { packet.onSuccess(request, response); },
  1313. error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
  1314. });
  1315. }
  1316. };
  1317. };
  1318. };
  1319. /**
  1320. * The JS object that exposes the comet API to applications
  1321. */
  1322. $.cometd = new $.Cometd(); // The default instance
  1323. })(jQuery);