RestChannels.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.cometd.RestChannels"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.cometd.RestChannels"] = true;
  8. dojo.provide("dojox.cometd.RestChannels");
  9. dojo.require("dojox.rpc.Client");
  10. dojo.requireIf(dojox.data && !!dojox.data.JsonRestStore,"dojox.data.restListener");
  11. // Note that cometd _base is _not_ required, this can run standalone, but ifyou want
  12. // cometd functionality, you must explicitly load/require it elsewhere, and cometd._base
  13. // MUST be loaded prior to RestChannels ifyou use it.
  14. // summary:
  15. // REST Channels - An HTTP/REST Based approach to Comet transport with full REST messaging
  16. // semantics
  17. // REST Channels is a efficient, reliable duplex transport for Comet
  18. // description:
  19. // This can be used:
  20. // 1. As a cometd transport
  21. // 2. As an enhancement for the REST RPC service, to enable "live" data (real-time updates directly alter the data in indexes)
  22. // 2a. With the JsonRestStore (which is driven by the REST RPC service), so this dojo.data has real-time data. Updates can be heard through the dojo.data notification API.
  23. // 3. As a standalone transport. To use it as a standalone transport looks like this:
  24. // | dojox.cometd.RestChannels.open();
  25. // | dojox.cometd.RestChannels.get("/myResource",{callback:function(){
  26. // | // this is called when the resource is first retrieved and any time the
  27. // | // resource is changed in the future. This provides a means for retrieving a
  28. // | // resource and subscribing to it in a single request
  29. // | });
  30. // | dojox.cometd.RestChannels.subscribe("/anotherResource",{callback:function(){
  31. // | // this is called when the resource is changed in the future
  32. // | });
  33. // Channels HTTP can be configured to a different delays:
  34. // | dojox.cometd.RestChannels.defaultInstance.autoReconnectTime = 60000; // reconnect after one minute
  35. //
  36. (function(){
  37. dojo.declare("dojox.cometd.RestChannels", null, {
  38. constructor: function(options){
  39. // summary:
  40. // Initiates the REST Channels protocol
  41. // options:
  42. // Keyword arguments:
  43. // The *autoSubscribeRoot* parameter:
  44. // When this is set, all REST service requests that have this
  45. // prefix will be auto-subscribed. The default is '/' (all REST requests).
  46. // The *url* parameter:
  47. // This is the url to connect to for server-sent messages. The default
  48. // is "/channels".
  49. // The *autoReconnectTime* parameter:
  50. // This is amount time to wait to reconnect with a connection is broken
  51. // The *reloadDataOnReconnect* parameter:
  52. // This indicates whether RestChannels should re-download data when a connection
  53. // is restored (value of true), or if it should re-subscribe with retroactive subscriptions
  54. // (Subscribe-Since header) using HEAD requests (value of false). The
  55. // default is true.
  56. dojo.mixin(this,options);
  57. // If we have a Rest service available and we are auto subscribing, we will augment the Rest service
  58. if(dojox.rpc.Rest && this.autoSubscribeRoot){
  59. // override the default Rest handler so we can add subscription requests
  60. var defaultGet = dojox.rpc.Rest._get;
  61. var self = this;
  62. dojox.rpc.Rest._get = function(service, id){
  63. // when there is a REST get, we will intercept and add our own xhr handler
  64. var defaultXhrGet = dojo.xhrGet;
  65. dojo.xhrGet = function(r){
  66. var autoSubscribeRoot = self.autoSubscribeRoot;
  67. return (autoSubscribeRoot && r.url.substring(0, autoSubscribeRoot.length) == autoSubscribeRoot) ?
  68. self.get(r.url,r) : // auto-subscribe
  69. defaultXhrGet(r); // plain XHR request
  70. };
  71. var result = defaultGet.apply(this,arguments);
  72. dojo.xhrGet = defaultXhrGet;
  73. return result;
  74. };
  75. }
  76. },
  77. absoluteUrl: function(baseUrl,relativeUrl){
  78. return new dojo._Url(baseUrl,relativeUrl)+'';
  79. },
  80. acceptType: "application/rest+json,application/http;q=0.9,*/*;q=0.7",
  81. subscriptions: {},
  82. subCallbacks: {},
  83. autoReconnectTime: 3000,
  84. reloadDataOnReconnect: true,
  85. sendAsJson: false,
  86. url: '/channels',
  87. autoSubscribeRoot: '/',
  88. open: function(){
  89. // summary:
  90. // Startup the transport (connect to the "channels" resource to receive updates from the server).
  91. //
  92. // description:
  93. // Note that if there is no connection open, this is automatically called when you do a subscription,
  94. // it is often not necessary to call this
  95. //
  96. this.started = true;
  97. if(!this.connected){
  98. this.connectionId = dojox.rpc.Client.clientId;
  99. var clientIdHeader = this.createdClientId ? 'Client-Id' : 'Create-Client-Id';
  100. this.createdClientId = true;
  101. var headers = {Accept:this.acceptType};
  102. headers[clientIdHeader] = this.connectionId;
  103. var dfd = dojo.xhrPost({headers:headers, url: this.url, noStatus: true});
  104. var self = this;
  105. this.lastIndex = 0;
  106. var onerror, onprogress = function(data){ // get all the possible event handlers
  107. if(typeof dojo == 'undefined'){
  108. return null;// this can be called after dojo is unloaded, just do nothing in that case
  109. }
  110. if(xhr && xhr.status > 400){
  111. return onerror(true);
  112. }
  113. if(typeof data == 'string'){
  114. data = data.substring(self.lastIndex);
  115. }
  116. var contentType = xhr && (xhr.contentType || xhr.getResponseHeader("Content-Type")) || (typeof data != 'string' && "already json");
  117. var error = self.onprogress(xhr,data,contentType);
  118. if(error){
  119. if(onerror()){
  120. return new Error(error);
  121. }
  122. }
  123. if(!xhr || xhr.readyState==4){
  124. xhr = null;
  125. if(self.connected){
  126. self.connected = false;
  127. self.open();
  128. }
  129. }
  130. return data;
  131. };
  132. onerror = function(error){
  133. if(xhr && xhr.status == 409){
  134. // a 409 indicates that there is a multiple connections, and we need to poll
  135. console.log("multiple tabs/windows open, polling");
  136. self.disconnected();
  137. return null;
  138. }
  139. self.createdClientId = false;
  140. self.disconnected();
  141. return error;
  142. };
  143. dfd.addCallbacks(onprogress,onerror);
  144. var xhr = dfd.ioArgs.xhr; // this may not exist if we are not using XHR, but an alternate XHR plugin
  145. if(xhr){
  146. // if we are doing a monitorable XHR, we want to listen to streaming events
  147. xhr.onreadystatechange = function(){
  148. var responseText;
  149. try{
  150. if(xhr.readyState == 3){// only for progress, the deferred object will handle the finished responses
  151. self.readyState = 3;
  152. responseText = xhr.responseText;
  153. }
  154. } catch(e){
  155. }
  156. if(typeof responseText=='string'){
  157. onprogress(responseText);
  158. }
  159. }
  160. }
  161. if(window.attachEvent){// IE needs a little help with cleanup
  162. window.attachEvent("onunload",function(){
  163. self.connected= false;
  164. if(xhr){
  165. xhr.abort();
  166. }
  167. });
  168. }
  169. this.connected = true;
  170. }
  171. },
  172. _send: function(method,args,data){
  173. // fire an XHR with appropriate modification for JSON handling
  174. if(this.sendAsJson){
  175. // send use JSON Messaging
  176. args.postData = dojo.toJson({
  177. target:args.url,
  178. method:method,
  179. content: data,
  180. params:args.content,
  181. subscribe:args.headers["Subscribe"]
  182. });
  183. args.url = this.url;
  184. method = "POST";
  185. }else{
  186. args.postData = dojo.toJson(data);
  187. }
  188. return dojo.xhr(method,args,args.postData);
  189. },
  190. subscribe: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
  191. // summary:
  192. // Subscribes to a channel/uri, and returns a dojo.Deferred object for the response from
  193. // the subscription request
  194. //
  195. // channel:
  196. // the uri for the resource you want to monitor
  197. //
  198. // args:
  199. // See dojo.xhr
  200. //
  201. // headers:
  202. // These are the headers to be applied to the channel subscription request
  203. //
  204. // callback:
  205. // This will be called when a event occurs for the channel
  206. // The callback will be called with a single argument:
  207. // | callback(message)
  208. // where message is an object that follows the XHR API:
  209. // status : Http status
  210. // statusText : Http status text
  211. // getAllResponseHeaders() : The response headers
  212. // getResponseHeaders(headerName) : Retrieve a header by name
  213. // responseText : The response body as text
  214. // with the following additional Bayeux properties
  215. // data : The response body as JSON
  216. // channel : The channel/url of the response
  217. args = args || {};
  218. args.url = this.absoluteUrl(this.url, channel);
  219. if(args.headers){
  220. // FIXME: combining Ranges with notifications is very complicated, we will save that for a future version
  221. delete args.headers.Range;
  222. }
  223. var oldSince = this.subscriptions[channel];
  224. var method = args.method || "HEAD"; // HEAD is the default for a subscription
  225. var since = args.since;
  226. var callback = args.callback;
  227. var headers = args.headers || (args.headers = {});
  228. this.subscriptions[channel] = since || oldSince || 0;
  229. var oldCallback = this.subCallbacks[channel];
  230. if(callback){
  231. this.subCallbacks[channel] = oldCallback ? function(m){
  232. oldCallback(m);
  233. callback(m);
  234. } : callback;
  235. }
  236. if(!this.connected){
  237. this.open();
  238. }
  239. if(oldSince === undefined || oldSince != since){
  240. headers["Cache-Control"] = "max-age=0";
  241. since = typeof since == 'number' ? new Date(since).toUTCString() : since;
  242. if(since){
  243. headers["Subscribe-Since"] = since;
  244. }
  245. headers["Subscribe"] = args.unsubscribe ? 'none' : '*';
  246. var dfd = this._send(method,args);
  247. var self = this;
  248. dfd.addBoth(function(result){
  249. var xhr = dfd.ioArgs.xhr;
  250. if(!(result instanceof Error)){
  251. if(args.confirmation){
  252. args.confirmation();
  253. }
  254. }
  255. if(xhr && xhr.getResponseHeader("Subscribed") == "OK"){
  256. var lastMod = xhr.getResponseHeader('Last-Modified');
  257. if(xhr.responseText){
  258. self.subscriptions[channel] = lastMod || new Date().toUTCString();
  259. }else{
  260. return null; // don't process the response, the response will be received in the main channels response
  261. }
  262. }else if(xhr && !(result instanceof Error)){ // if the server response was successful and we have access to headers but it does indicate a subcription was successful, that means it is did not accept the subscription
  263. delete self.subscriptions[channel];
  264. }
  265. if(!(result instanceof Error)){
  266. var message = {
  267. responseText:xhr && xhr.responseText,
  268. channel:channel,
  269. getResponseHeader:function(name){
  270. return xhr.getResponseHeader(name);
  271. },
  272. getAllResponseHeaders:function(){
  273. return xhr.getAllResponseHeaders();
  274. },
  275. result: result
  276. };
  277. if(self.subCallbacks[channel]){
  278. self.subCallbacks[channel](message); // call with the fake xhr object
  279. }
  280. }else{
  281. if(self.subCallbacks[channel]){
  282. self.subCallbacks[channel](xhr); // call with the actual xhr object
  283. }
  284. }
  285. return result;
  286. });
  287. return dfd;
  288. }
  289. return null;
  290. },
  291. publish: function(channel,data){
  292. // summary:
  293. // Publish an event.
  294. // description:
  295. // This does a simple POST operation to the provided URL,
  296. // POST is the semantic equivalent of publishing a message within REST/Channels
  297. // channel:
  298. // Channel/resource path to publish to
  299. // data:
  300. // data to publish
  301. return this._send("POST",{url:channel,contentType : 'application/json'},data);
  302. },
  303. _processMessage: function(message){
  304. message.event = message.event || message.getResponseHeader('Event');
  305. if(message.event=="connection-conflict"){
  306. return "conflict"; // indicate an error
  307. }
  308. try{
  309. message.result = message.result || dojo.fromJson(message.responseText);
  310. }
  311. catch(e){}
  312. var self = this;
  313. var loc = message.channel = new dojo._Url(this.url, message.source || message.getResponseHeader('Content-Location'))+'';//for cometd
  314. if(loc in this.subscriptions && message.getResponseHeader){
  315. this.subscriptions[loc] = message.getResponseHeader('Last-Modified');
  316. }
  317. if(this.subCallbacks[loc]){
  318. setTimeout(function(){ //give it it's own stack
  319. self.subCallbacks[loc](message);
  320. },0);
  321. }
  322. this.receive(message);
  323. return null;
  324. },
  325. onprogress: function(xhr,data,contentType){
  326. // internal XHR progress handler
  327. if(!contentType || contentType.match(/application\/rest\+json/)){
  328. var size = data.length;
  329. data = data.replace(/^\s*[,\[]?/,'['). // must start with a opening bracket
  330. replace(/[,\]]?\s*$/,']'); // and end with a closing bracket
  331. try{
  332. // if this fails, it probably means we have an incomplete JSON object
  333. var xhrs = dojo.fromJson(data);
  334. this.lastIndex += size;
  335. }
  336. catch(e){
  337. }
  338. }else if(dojox.io && dojox.io.httpParse && contentType.match(/application\/http/)){
  339. // do HTTP tunnel parsing
  340. var topHeaders = '';
  341. if(xhr && xhr.getAllResponseHeaders){
  342. // mixin/inherit headers from the container response
  343. topHeaders = xhr.getAllResponseHeaders();
  344. }
  345. xhrs = dojox.io.httpParse(data,topHeaders,xhr.readyState != 4);
  346. }else if(typeof data == "object"){
  347. xhrs = data;
  348. }
  349. if(xhrs){
  350. for(var i = 0;i < xhrs.length;i++){
  351. if(this._processMessage(xhrs[i])){
  352. return "conflict";
  353. }
  354. }
  355. return null;
  356. }
  357. if(!xhr){
  358. //no streaming and we didn't get any message, must be an error
  359. return "error";
  360. }
  361. if(xhr.readyState != 4){ // we only want finished responses here if we are not streaming
  362. return null;
  363. }
  364. if(xhr.__proto__){// firefox uses this property, so we create an instance to shadow this property
  365. xhr = {channel:"channel",__proto__:xhr};
  366. }
  367. return this._processMessage(xhr);
  368. },
  369. get: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
  370. // summary:
  371. // GET the initial value of the resource and subscribe to it
  372. // See subscribe for parameter values
  373. (args = args || {}).method = "GET";
  374. return this.subscribe(channel,args);
  375. },
  376. receive: function(message){
  377. // summary:
  378. // Called when a message is received from the server
  379. // message:
  380. // A cometd/XHR message
  381. if(dojox.data && dojox.data.restListener){
  382. dojox.data.restListener(message);
  383. }
  384. },
  385. disconnected: function(){
  386. // summary:
  387. // called when our channel gets disconnected
  388. var self = this;
  389. if(this.connected){
  390. this.connected = false;
  391. if(this.started){ // if we are started, we shall try to reconnect
  392. setTimeout(function(){ // auto reconnect
  393. // resubscribe to our current subscriptions
  394. var subscriptions = self.subscriptions;
  395. self.subscriptions = {};
  396. for(var i in subscriptions){
  397. if(self.reloadDataOnReconnect && dojox.rpc.JsonRest){
  398. // do a reload of the resource
  399. delete dojox.rpc.Rest._index[i];
  400. dojox.rpc.JsonRest.fetch(i);
  401. }else{
  402. self.subscribe(i,{since:subscriptions[i]});
  403. }
  404. }
  405. self.open();
  406. }, this.autoReconnectTime);
  407. }
  408. }
  409. },
  410. unsubscribe: function(/*String*/channel, /*dojo.__XhrArgs?*/args){
  411. // summary:
  412. // unsubscribes from the resource
  413. // See subscribe for parameter values
  414. args = args || {};
  415. args.unsubscribe = true;
  416. this.subscribe(channel,args); // change the time frame to after 5000AD
  417. },
  418. disconnect: function(){
  419. // summary:
  420. // disconnect from the server
  421. this.started = false;
  422. this.xhr.abort();
  423. }
  424. });
  425. var Channels = dojox.cometd.RestChannels.defaultInstance = new dojox.cometd.RestChannels();
  426. if(dojox.cometd.connectionTypes){
  427. // register as a dojox.cometd transport and wire everything for cometd handling
  428. // below are the necessary adaptions for cometd
  429. Channels.startup = function(data){ // must be able to handle objects or strings
  430. Channels.open();
  431. this._cometd._deliver({channel:"/meta/connect",successful:true}); // tell cometd we are connected so it can proceed to send subscriptions, even though we aren't yet
  432. };
  433. Channels.check = function(types, version, xdomain){
  434. for(var i = 0; i< types.length; i++){
  435. if(types[i] == "rest-channels"){
  436. return !xdomain;
  437. }
  438. }
  439. return false;
  440. };
  441. Channels.deliver = function(message){
  442. // nothing to do
  443. };
  444. dojo.connect(this,"receive",null,function(message){
  445. message.data = message.result;
  446. this._cometd._deliver(message);
  447. });
  448. Channels.sendMessages = function(messages){
  449. for(var i = 0; i < messages.length; i++){
  450. var message = messages[i];
  451. var channel = message.channel;
  452. var cometd = this._cometd;
  453. var args = {
  454. confirmation: function(){ // send a confirmation back to cometd
  455. cometd._deliver({channel:channel,successful:true});
  456. }
  457. };
  458. if(channel == '/meta/subscribe'){
  459. this.subscribe(message.subscription,args);
  460. }else if(channel == '/meta/unsubscribe'){
  461. this.unsubscribe(message.subscription,args);
  462. }else if(channel == '/meta/connect'){
  463. args.confirmation();
  464. }else if(channel == '/meta/disconnect'){
  465. Channels.disconnect();
  466. args.confirmation();
  467. }else if(channel.substring(0,6) != '/meta/'){
  468. this.publish(channel,message.data);
  469. }
  470. }
  471. };
  472. dojox.cometd.connectionTypes.register("rest-channels", Channels.check, Channels,false,true);
  473. }
  474. })();
  475. }