cometd.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. if (!dojo._hasResource["dojox._cometd.cometd"]) { // _hasResource checks added
  2. // by build. Do not use
  3. // _hasResource directly in
  4. // your code.
  5. dojo._hasResource["dojox._cometd.cometd"] = true;
  6. dojo.provide("dojox._cometd.cometd");
  7. dojo.require("dojo.AdapterRegistry");
  8. dojo.require("dojo.io.script");
  9. // FIXME: need to add local topic support to advise about:
  10. // successful handshake
  11. // network failure of channel
  12. // graceful disconnect
  13. /*
  14. * this file defines Comet protocol client. Actual message transport is
  15. * deferred to one of several connection type implementations. The default
  16. * is a long-polling implementation. A single global object named
  17. * "dojox.cometd" is used to mediate for these connection types in order to
  18. * provide a stable interface.
  19. */
  20. dojox.cometd = new function() {
  21. /*
  22. * cometd states: DISCONNECTED: _initialized==false && _connected==false
  23. * CONNECTING: _initialized==true && _connected==false (handshake sent)
  24. * CONNECTED: _initialized==true && _connected==true (first successful
  25. * connect) DISCONNECTING: _initialized==false && _connected==true
  26. * (disconnect sent)
  27. */
  28. this._initialized = false;
  29. this._connected = false;
  30. this._polling = false;
  31. this.connectionTypes = new dojo.AdapterRegistry(true);
  32. this.version = "1.0";
  33. this.minimumVersion = "0.9";
  34. this.clientId = null;
  35. this.messageId = 0;
  36. this.batch = 0;
  37. this._isXD = false;
  38. this.handshakeReturn = null;
  39. this.currentTransport = null;
  40. this.url = null;
  41. this.lastMessage = null;
  42. this.topics = {};
  43. this._messageQ = [];
  44. this.handleAs = "json-comment-optional";
  45. this.advice;
  46. this.pendingSubscriptions = {}
  47. this.pendingUnsubscriptions = {}
  48. this._subscriptions = [];
  49. this.tunnelInit = function(childLocation, childDomain) {
  50. // placeholder
  51. }
  52. this.tunnelCollapse = function() {
  53. console.debug("tunnel collapsed!");
  54. // placeholder
  55. }
  56. this.init = function(root, props, bargs) {
  57. // FIXME: if the root isn't from the same host, we should
  58. // automatically
  59. // try to select an XD-capable transport
  60. props = props || {};
  61. // go ask the short bus server what we can support
  62. props.version = this.version;
  63. props.minimumVersion = this.minimumVersion;
  64. props.channel = "/meta/handshake";
  65. props.id = "" + this.messageId++;
  66. this.url = root || djConfig["cometdRoot"];
  67. if (!this.url) {
  68. console
  69. .debug("no cometd root specified in djConfig and no root passed");
  70. return;
  71. }
  72. // Are we x-domain? borrowed from dojo.uri.Uri in lieu of fixed host
  73. // and port properties
  74. var regexp = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$";
  75. var r = ("" + window.location).match(new RegExp(regexp));
  76. if (r[4]) {
  77. var tmp = r[4].split(":");
  78. var thisHost = tmp[0];
  79. var thisPort = tmp[1] || "80"; // FIXME: match 443
  80. r = this.url.match(new RegExp(regexp));
  81. if (r[4]) {
  82. tmp = r[4].split(":");
  83. var urlHost = tmp[0];
  84. var urlPort = tmp[1] || "80";
  85. this._isXD = ((urlHost != thisHost) || (urlPort != thisPort));
  86. }
  87. }
  88. if (!this._isXD) {
  89. if (props.ext) {
  90. if (props.ext["json-comment-filtered"] !== true
  91. && props.ext["json-comment-filtered"] !== false) {
  92. props.ext["json-comment-filtered"] = true;
  93. }
  94. } else {
  95. props.ext = {
  96. "json-comment-filtered" : true
  97. };
  98. }
  99. }
  100. var bindArgs = {
  101. url : this.url,
  102. handleAs : this.handleAs,
  103. content : {
  104. "message" : dojo.toJson([props])
  105. },
  106. load : dojo.hitch(this, "finishInit"),
  107. error : function(e) {
  108. console.debug("handshake error!:", e);
  109. }
  110. };
  111. if (bargs) {
  112. dojo.mixin(bindArgs, bargs);
  113. }
  114. this._props = props;
  115. this._initialized = true;
  116. this.batch = 0;
  117. this.startBatch();
  118. // if xdomain, then we assume jsonp for handshake
  119. if (this._isXD) {
  120. bindArgs.callbackParamName = "jsonp";
  121. return dojo.io.script.get(bindArgs);
  122. }
  123. return dojo.xhrPost(bindArgs);
  124. }
  125. this.finishInit = function(data) {
  126. data = data[0];
  127. this.handshakeReturn = data;
  128. // pick a transport
  129. if (data["advice"]) {
  130. this.advice = data.advice;
  131. }
  132. if (!data.successful) {
  133. console.debug("cometd init failed");
  134. if (this.advice && this.advice["reconnect"] == "none") {
  135. return;
  136. }
  137. if (this.advice && this.advice["interval"]
  138. && this.advice.interval > 0) {
  139. var cometd = this;
  140. setTimeout(function() {
  141. cometd.init(cometd.url, cometd._props);
  142. }, this.advice.interval);
  143. } else {
  144. this.init(this.url, this._props);
  145. }
  146. return;
  147. }
  148. if (data.version < this.minimumVersion) {
  149. console.debug("cometd protocol version mismatch. We wanted",
  150. this.minimumVersion, "but got", data.version);
  151. return;
  152. }
  153. this.currentTransport = this.connectionTypes.match(
  154. data.supportedConnectionTypes, data.version, this._isXD);
  155. this.currentTransport._cometd = this;
  156. this.currentTransport.version = data.version;
  157. this.clientId = data.clientId;
  158. this.tunnelInit = dojo.hitch(this.currentTransport, "tunnelInit");
  159. this.tunnelCollapse = dojo.hitch(this.currentTransport,
  160. "tunnelCollapse");
  161. this.currentTransport.startup(data);
  162. }
  163. // public API functions called by cometd or by the transport classes
  164. this.deliver = function(messages) {
  165. // console.debug(messages);
  166. dojo.forEach(messages, this._deliver, this);
  167. return messages;
  168. }
  169. this._deliver = function(message) {
  170. // dipatch events along the specified path
  171. if (!message["channel"]) {
  172. if (message["success"] !== true) {
  173. console.debug("cometd error: no channel for message!",
  174. message);
  175. return;
  176. }
  177. }
  178. this.lastMessage = message;
  179. if (message.advice) {
  180. this.advice = message.advice; // TODO maybe merge?
  181. }
  182. // check to see if we got a /meta channel message that we care about
  183. if ((message["channel"]) && (message.channel.length > 5)
  184. && (message.channel.substr(0, 5) == "/meta")) {
  185. // check for various meta topic actions that we need to respond
  186. // to
  187. switch (message.channel) {
  188. case "/meta/connect" :
  189. if (message.successful && !this._connected) {
  190. this._connected = this._initialized;
  191. this.endBatch();
  192. } else if (!this._initialized) {
  193. this._connected = false; // finish disconnect
  194. }
  195. break;
  196. case "/meta/subscribe" :
  197. var pendingDef = this.pendingSubscriptions[message.subscription];
  198. if (!message.successful) {
  199. if (pendingDef) {
  200. pendingDef.errback(new Error(message.error));
  201. delete this.pendingSubscriptions[message.subscription];
  202. }
  203. return;
  204. }
  205. dojox.cometd.subscribed(message.subscription, message);
  206. if (pendingDef) {
  207. pendingDef.callback(true);
  208. delete this.pendingSubscriptions[message.subscription];
  209. }
  210. break;
  211. case "/meta/unsubscribe" :
  212. var pendingDef = this.pendingUnsubscriptions[message.subscription];
  213. if (!message.successful) {
  214. if (pendingDef) {
  215. pendingDef.errback(new Error(message.error));
  216. delete this.pendingUnsubscriptions[message.subscription];
  217. }
  218. return;
  219. }
  220. this.unsubscribed(message.subscription, message);
  221. if (pendingDef) {
  222. pendingDef.callback(true);
  223. delete this.pendingUnsubscriptions[message.subscription];
  224. }
  225. break;
  226. }
  227. }
  228. // send the message down for processing by the transport
  229. this.currentTransport.deliver(message);
  230. if (message.data) {
  231. // dispatch the message to any locally subscribed listeners
  232. var tname = "/cometd" + message.channel;
  233. dojo.publish(tname, [message]);
  234. }
  235. }
  236. this.disconnect = function() {
  237. dojo.forEach(this._subscriptions, dojo.unsubscribe);
  238. this._subscriptions = [];
  239. this._messageQ = [];
  240. if (this._initialized && this.currentTransport) {
  241. this._initialized = false;
  242. this.currentTransport.disconnect();
  243. }
  244. this._initialized = false;
  245. if (!this._polling)
  246. this._connected = false;
  247. }
  248. // public API functions called by end users
  249. this.publish = function(/* string */channel, /* object */data, /* object */
  250. properties) {
  251. // summary:
  252. // publishes the passed message to the cometd server for delivery
  253. // on the specified topic
  254. // channel:
  255. // the destination channel for the message
  256. // data:
  257. // a JSON object containing the message "payload"
  258. // properties:
  259. // Optional. Other meta-data to be mixed into the top-level of the
  260. // message
  261. var message = {
  262. data : data,
  263. channel : channel
  264. };
  265. if (properties) {
  266. dojo.mixin(message, properties);
  267. }
  268. this._sendMessage(message);
  269. }
  270. this._sendMessage = function(/* object */message) {
  271. if (this.currentTransport && this._connected && this.batch == 0) {
  272. return this.currentTransport.sendMessages([message]);
  273. } else {
  274. this._messageQ.push(message);
  275. }
  276. }
  277. this.subscribe = function( /* string */channel,
  278. /* object, optional */objOrFunc,
  279. /* string, optional */funcName) { // return: boolean
  280. // summary:
  281. // inform the server of this client's interest in channel
  282. // channel:
  283. // name of the cometd channel to subscribe to
  284. // objOrFunc:
  285. // an object scope for funcName or the name or reference to a
  286. // function to be called when messages are delivered to the
  287. // channel
  288. // funcName:
  289. // the second half of the objOrFunc/funcName pair for identifying
  290. // a callback function to notifiy upon channel message delivery
  291. if (this.pendingSubscriptions[channel]) {
  292. // We already asked to subscribe to this channel, and
  293. // haven't heard back yet. Fail the previous attempt.
  294. var oldDef = this.pendingSubscriptions[channel];
  295. oldDef.cancel();
  296. delete this.pendingSubscriptions[channel];
  297. }
  298. var pendingDef = new dojo.Deferred();
  299. this.pendingSubscriptions[channel] = pendingDef;
  300. if (objOrFunc) {
  301. var tname = "/cometd" + channel;
  302. if (this.topics[tname]) {
  303. dojo.unsubscribe(this.topics[tname]);
  304. }
  305. var topic = dojo.subscribe(tname, objOrFunc, funcName);
  306. this.topics[tname] = topic;
  307. }
  308. this._sendMessage({
  309. channel : "/meta/subscribe",
  310. subscription : channel
  311. });
  312. return pendingDef;
  313. }
  314. this.subscribed = function( /* string */channel,
  315. /* obj */message) {
  316. }
  317. this.unsubscribe = function(/* string */channel) { // return: boolean
  318. // summary:
  319. // inform the server of this client's disinterest in channel
  320. // channel:
  321. // name of the cometd channel to unsubscribe from
  322. if (this.pendingUnsubscriptions[channel]) {
  323. // We already asked to unsubscribe from this channel, and
  324. // haven't heard back yet. Fail the previous attempt.
  325. var oldDef = this.pendingUnsubscriptions[channel];
  326. oldDef.cancel();
  327. delete this.pendingUnsubscriptions[channel];
  328. }
  329. var pendingDef = new dojo.Deferred();
  330. this.pendingUnsubscriptions[channel] = pendingDef;
  331. var tname = "/cometd" + channel;
  332. if (this.topics[tname]) {
  333. dojo.unsubscribe(this.topics[tname]);
  334. }
  335. this._sendMessage({
  336. channel : "/meta/unsubscribe",
  337. subscription : channel
  338. });
  339. return pendingDef;
  340. }
  341. this.unsubscribed = function( /* string */channel,
  342. /* obj */message) {
  343. }
  344. this.startBatch = function() {
  345. this.batch++;
  346. }
  347. this.endBatch = function() {
  348. if (--this.batch <= 0 && this.currentTransport && this._connected) {
  349. this.batch = 0;
  350. var messages = this._messageQ;
  351. this._messageQ = [];
  352. if (messages.length > 0) {
  353. this.currentTransport.sendMessages(messages);
  354. }
  355. }
  356. }
  357. this._onUnload = function() {
  358. // make this the last of the onUnload method
  359. dojo.addOnUnload(dojox.cometd, "disconnect");
  360. }
  361. }
  362. /*
  363. * transport objects MUST expose the following methods: - check - startup -
  364. * sendMessages - deliver - disconnect optional, standard but transport
  365. * dependent methods are: - tunnelCollapse - tunnelInit
  366. *
  367. * Transports SHOULD be namespaced under the cometd object and transports
  368. * MUST register themselves with cometd.connectionTypes
  369. *
  370. * here's a stub transport defintion:
  371. *
  372. * cometd.blahTransport = new function(){ this._connectionType="my-polling";
  373. * this._cometd=null; this.lastTimestamp = null;
  374. *
  375. * this.check = function(types, version, xdomain){ // summary: // determines
  376. * whether or not this transport is suitable given a // list of transport
  377. * types that the server supports return dojo.lang.inArray(types, "blah"); }
  378. *
  379. * this.startup = function(){ if(dojox.cometd._polling){ return; } // FIXME:
  380. * fill in startup routine here dojox.cometd._polling = true; }
  381. *
  382. * this.sendMessages = function(message){ // FIXME: fill in message array
  383. * sending logic }
  384. *
  385. * this.deliver = function(message){ if(message["timestamp"]){
  386. * this.lastTimestamp = message.timestamp; } if( (message.channel.length >
  387. * 5)&& (message.channel.substr(0, 5) == "/meta")){ // check for various
  388. * meta topic actions that we need to respond to // switch(message.channel){ //
  389. * case "/meta/connect": // // FIXME: fill in logic here // break; // //
  390. * case ...: ... // } } }
  391. *
  392. * this.disconnect = function(){ } } cometd.connectionTypes.register("blah",
  393. * cometd.blahTransport.check, cometd.blahTransport);
  394. */
  395. dojox.cometd.longPollTransport = new function() {
  396. this._connectionType = "long-polling";
  397. this._cometd = null;
  398. this.lastTimestamp = null;
  399. this.check = function(types, version, xdomain) {
  400. return ((!xdomain) && (dojo.indexOf(types, "long-polling") >= 0));
  401. }
  402. this.tunnelInit = function() {
  403. if (this._cometd._polling) {
  404. return;
  405. }
  406. this.openTunnelWith({
  407. message : dojo.toJson([{
  408. channel : "/meta/connect",
  409. clientId : this._cometd.clientId,
  410. connectionType : this._connectionType,
  411. id : "" + this._cometd.messageId++
  412. }])
  413. });
  414. }
  415. this.tunnelCollapse = function() {
  416. if (!this._cometd._polling) {
  417. // try to restart the tunnel
  418. this._cometd._polling = false;
  419. // TODO handle transport specific advice
  420. if (this._cometd["advice"]) {
  421. if (this._cometd.advice["reconnect"] == "none") {
  422. return;
  423. }
  424. if ((this._cometd.advice["interval"])
  425. && (this._cometd.advice.interval > 0)) {
  426. var transport = this;
  427. setTimeout(function() {
  428. transport._connect();
  429. }, this._cometd.advice.interval);
  430. } else {
  431. this._connect();
  432. }
  433. } else {
  434. this._connect();
  435. }
  436. }
  437. }
  438. this._connect = function() {
  439. if ((this._cometd["advice"])
  440. && (this._cometd.advice["reconnect"] == "handshake")) {
  441. this._cometd.init(this._cometd.url, this._cometd._props);
  442. } else if (this._cometd._connected) {
  443. this.openTunnelWith({
  444. message : dojo.toJson([{
  445. channel : "/meta/connect",
  446. connectionType : this._connectionType,
  447. clientId : this._cometd.clientId,
  448. timestamp : this.lastTimestamp,
  449. id : "" + this._cometd.messageId++
  450. }])
  451. });
  452. }
  453. }
  454. this.deliver = function(message) {
  455. // console.debug(message);
  456. if (message["timestamp"]) {
  457. this.lastTimestamp = message.timestamp;
  458. }
  459. }
  460. this.openTunnelWith = function(content, url) {
  461. // console.debug("openTunnelWith:", content, (url||cometd.url));
  462. var d = dojo.xhrPost({
  463. url : (url || this._cometd.url),
  464. content : content,
  465. handleAs : this._cometd.handleAs,
  466. load : dojo.hitch(this, function(data) {
  467. // console.debug(evt.responseText);
  468. // console.debug(data);
  469. this._cometd._polling = false;
  470. this._cometd.deliver(data);
  471. this.tunnelCollapse();
  472. }),
  473. error : function(err) {
  474. console.debug("tunnel opening failed:", err);
  475. dojo.cometd._polling = false;
  476. // TODO - follow advice to reconnect or rehandshake?
  477. }
  478. });
  479. this._cometd._polling = true;
  480. }
  481. this.sendMessages = function(messages) {
  482. for (var i = 0; i < messages.length; i++) {
  483. messages[i].clientId = this._cometd.clientId;
  484. messages[i].id = "" + this._cometd.messageId++;
  485. }
  486. return dojo.xhrPost({
  487. url : this._cometd.url || djConfig["cometdRoot"],
  488. handleAs : this._cometd.handleAs,
  489. load : dojo.hitch(this._cometd, "deliver"),
  490. content : {
  491. message : dojo.toJson(messages)
  492. }
  493. });
  494. }
  495. this.startup = function(handshakeData) {
  496. if (this._cometd._connected) {
  497. return;
  498. }
  499. this.tunnelInit();
  500. }
  501. this.disconnect = function() {
  502. dojo.xhrPost({
  503. url : this._cometd.url || djConfig["cometdRoot"],
  504. handleAs : this._cometd.handleAs,
  505. content : {
  506. message : dojo.toJson([{
  507. channel : "/meta/disconnect",
  508. clientId : this._cometd.clientId,
  509. id : "" + this._cometd.messageId++
  510. }])
  511. }
  512. });
  513. }
  514. }
  515. dojox.cometd.callbackPollTransport = new function() {
  516. this._connectionType = "callback-polling";
  517. this._cometd = null;
  518. this.lastTimestamp = null;
  519. this.check = function(types, version, xdomain) {
  520. // we handle x-domain!
  521. return (dojo.indexOf(types, "callback-polling") >= 0);
  522. }
  523. this.tunnelInit = function() {
  524. if (this._cometd._polling) {
  525. return;
  526. }
  527. this.openTunnelWith({
  528. message : dojo.toJson([{
  529. channel : "/meta/connect",
  530. clientId : this._cometd.clientId,
  531. connectionType : this._connectionType,
  532. id : "" + this._cometd.messageId++
  533. }])
  534. });
  535. }
  536. this.tunnelCollapse = dojox.cometd.longPollTransport.tunnelCollapse;
  537. this._connect = dojox.cometd.longPollTransport._connect;
  538. this.deliver = dojox.cometd.longPollTransport.deliver;
  539. this.openTunnelWith = function(content, url) {
  540. // create a <script> element to generate the request
  541. dojo.io.script.get({
  542. load : dojo.hitch(this, function(data) {
  543. this._cometd._polling = false;
  544. this._cometd.deliver(data);
  545. this.tunnelCollapse();
  546. }),
  547. error : function() {
  548. this._cometd._polling = false;
  549. console.debug("tunnel opening failed");
  550. },
  551. url : (url || this._cometd.url),
  552. content : content,
  553. callbackParamName : "jsonp"
  554. });
  555. this._cometd._polling = true;
  556. }
  557. this.sendMessages = function(/* array */messages) {
  558. for (var i = 0; i < messages.length; i++) {
  559. messages[i].clientId = this._cometd.clientId;
  560. messages[i].id = "" + this._cometd.messageId++;
  561. }
  562. var bindArgs = {
  563. url : this._cometd.url || djConfig["cometdRoot"],
  564. load : dojo.hitch(this._cometd, "deliver"),
  565. callbackParamName : "jsonp",
  566. content : {
  567. message : dojo.toJson(messages)
  568. }
  569. };
  570. return dojo.io.script.get(bindArgs);
  571. }
  572. this.startup = function(handshakeData) {
  573. if (this._cometd._connected) {
  574. return;
  575. }
  576. this.tunnelInit();
  577. }
  578. this.disconnect = dojox.cometd.longPollTransport.disconnect;
  579. this.disconnect = function() {
  580. dojo.io.script.get({
  581. url : this._cometd.url || djConfig["cometdRoot"],
  582. callbackParamName : "jsonp",
  583. content : {
  584. message : dojo.toJson([{
  585. channel : "/meta/disconnect",
  586. clientId : this._cometd.clientId,
  587. id : "" + this._cometd.messageId++
  588. }])
  589. }
  590. });
  591. }
  592. }
  593. dojox.cometd.connectionTypes.register("long-polling",
  594. dojox.cometd.longPollTransport.check,
  595. dojox.cometd.longPollTransport);
  596. dojox.cometd.connectionTypes.register("callback-polling",
  597. dojox.cometd.callbackPollTransport.check,
  598. dojox.cometd.callbackPollTransport);
  599. dojo.addOnUnload(dojox.cometd, "_onUnload");
  600. }