/tags/1.0/src/main/webapp/js/jquery.stream.js

http://jquery-stream.googlecode.com/ · JavaScript · 511 lines · 363 code · 77 blank · 71 comment · 50 complexity · 1152d82ad78b415289c24a243c55eb6a MD5 · raw file

  1. /*
  2. * jQuery Stream @VERSION
  3. * Comet Streaming JavaScript Library
  4. * http://code.google.com/p/jquery-stream/
  5. *
  6. * Copyright 2011, Donghwan Kim
  7. * Licensed under the Apache License, Version 2.0
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Compatible with jQuery 1.4+
  11. */
  12. (function($, undefined) {
  13. // Does the throbber of doom exist?
  14. var throbber = $.browser.webkit && !$.isReady;
  15. if (throbber) {
  16. $(window).load(function() {
  17. throbber = false;
  18. });
  19. }
  20. // Stream is based on The WebSocket API
  21. // http://dev.w3.org/html5/websockets/
  22. function Stream(url, options) {
  23. // Assigns url and merges options
  24. this.url = url;
  25. this.options = $.extend(true, {}, this.options, options);
  26. for (var i in {open: 1, message: 1, error: 1, close: 1}) {
  27. this.options[i] = $.makeArray(this.options[i]);
  28. }
  29. // The url is a identifier of this instance within the document
  30. Stream.instances[this.url] = this;
  31. var self = this;
  32. if (!throbber) {
  33. setTimeout(function() {
  34. self.open();
  35. }, 0);
  36. } else {
  37. switch (this.options.throbber.type || this.options.throbber) {
  38. case "lazy":
  39. $(window).load(function() {
  40. setTimeout(function() {
  41. self.open();
  42. }, self.options.throbber.delay || 50);
  43. });
  44. break;
  45. case "reconnect":
  46. self.open();
  47. $(window).load(function() {
  48. if (self.readyState === 0) {
  49. self.options.open.push(function() {
  50. self.options.open.pop();
  51. setTimeout(function() {
  52. reconnect();
  53. }, 10);
  54. });
  55. } else {
  56. reconnect();
  57. }
  58. function reconnect() {
  59. self.options.close.push(function() {
  60. self.options.close.pop();
  61. setTimeout(function() {
  62. self.readyState = 0;
  63. self.open();
  64. }, self.options.throbber.delay || 50);
  65. });
  66. var reconn = self.options.reconnect;
  67. self.close();
  68. self.options.reconnect = reconn;
  69. }
  70. });
  71. break;
  72. }
  73. }
  74. }
  75. $.extend(Stream.prototype, {
  76. // Default options
  77. options: {
  78. // Whether to automatically reconnect when connection closed
  79. reconnect: true,
  80. // Only for WebKit
  81. throbber: "lazy",
  82. // Message data type
  83. dataType: "text",
  84. // Message data converters
  85. converters: {
  86. text: window.String,
  87. // jQuery.parseJSON is in jQuery 1.4.1
  88. json: $.parseJSON,
  89. // jQuery.parseXML is in jQuery 1.5
  90. xml: $.parseXML
  91. }
  92. },
  93. // Current stream connection's identifier within the server
  94. id: null,
  95. // The state of the connection
  96. // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
  97. readyState: 0,
  98. send: function(data) {
  99. if (this.readyState === 0) {
  100. $.error("INVALID_STATE_ERR: Stream not open");
  101. }
  102. if (arguments.length) {
  103. // Converts data if not already a string and pushes it into the data queue
  104. this.dataQueue.push(!data ?
  105. "" :
  106. ((typeof data === "string" ? data : $.param(data, $.ajaxSettings.traditional)) + "&"));
  107. }
  108. if (this.sending !== true) {
  109. this.sending = true;
  110. // Performs an Ajax iterating through the data queue
  111. (function post() {
  112. if (this.readyState === 1 && this.dataQueue.length) {
  113. $.ajax({
  114. url: this.url,
  115. context: this,
  116. type: "POST",
  117. data: this.dataQueue.shift() + this.paramMetadata("send"),
  118. complete: post
  119. });
  120. } else {
  121. this.sending = false;
  122. }
  123. }).call(this);
  124. }
  125. },
  126. close: function() {
  127. // Do nothing if the readyState is in the CLOSING or CLOSED
  128. if (this.readyState < 2) {
  129. this.readyState = 2;
  130. // Notifies the server
  131. $.post(this.url, this.paramMetadata("close"));
  132. // Prevents reconnecting
  133. this.options.reconnect = false;
  134. this.abort();
  135. }
  136. },
  137. paramMetadata: function(type, props) {
  138. // Always includes connection id and communication type
  139. props = $.extend({}, props, {id: this.id, type: type});
  140. var answer = {};
  141. for (var key in props) {
  142. answer["metadata." + key] = props[key];
  143. }
  144. return $.param(answer);
  145. },
  146. handleResponse: function(text) {
  147. if (this.readyState === 0) {
  148. // The top of the response is made up of the id and padding
  149. this.id = text.substring(0, text.indexOf(";"));
  150. this.message = {index: text.indexOf(";", this.id.length + 1) + 1, size: null, data: ""};
  151. this.dataQueue = this.dataQueue || [];
  152. this.readyState = 1;
  153. this.trigger("open");
  154. }
  155. // Parses messages
  156. // message format = message-size ; message-data ;
  157. for(;;) {
  158. if (this.message.size == null) {
  159. // Checks a semicolon of size part
  160. var sizeEnd = text.indexOf(";", this.message.index);
  161. if (sizeEnd < 0) {
  162. return;
  163. }
  164. this.message.size = +text.substring(this.message.index, sizeEnd);
  165. this.message.index = sizeEnd + 1;
  166. }
  167. var data = text.substr(this.message.index, this.message.size - this.message.data.length);
  168. this.message.data += data;
  169. this.message.index += data.length;
  170. // Has this message been completed?
  171. if (this.message.size !== this.message.data.length) {
  172. return;
  173. }
  174. // Checks a semicolon of data part
  175. var dataEnd = text.indexOf(";", this.message.index);
  176. if (dataEnd < 0) {
  177. return;
  178. }
  179. this.message.index = dataEnd + 1;
  180. // Converts the data type
  181. this.message.data = this.options.converters[this.options.dataType](this.message.data);
  182. if (this.readyState < 3) {
  183. // Pseudo MessageEvent
  184. this.trigger("message", {
  185. data: this.message.data,
  186. origin: "",
  187. lastEventId: "",
  188. source: null,
  189. ports: null,
  190. openMessageEvent: $.noop
  191. });
  192. }
  193. // Resets the data and size
  194. this.message.size = null;
  195. this.message.data = "";
  196. }
  197. },
  198. handleClose: function(isError) {
  199. var readyState = this.readyState;
  200. this.readyState = 3;
  201. if (isError === true) {
  202. // Prevents reconnecting
  203. this.options.reconnect = false;
  204. switch (readyState) {
  205. // If establishing a connection fails, fires the close event instead of the error event
  206. case 0:
  207. // Pseudo CloseEvent
  208. this.trigger("close", {
  209. wasClean: false,
  210. code: "",
  211. reason: "",
  212. initCloseEvent: $.noop
  213. });
  214. break;
  215. case 1:
  216. case 2:
  217. this.trigger("error");
  218. break;
  219. }
  220. } else {
  221. // Pseudo CloseEvent
  222. this.trigger("close", {
  223. // Presumes that the stream closed cleanly
  224. wasClean: true,
  225. code: "",
  226. reason: "",
  227. initCloseEvent: $.noop
  228. });
  229. // Reconnect?
  230. if (this.options.reconnect === true) {
  231. this.readyState = 0;
  232. this.open();
  233. }
  234. }
  235. },
  236. trigger: function(type, props) {
  237. var event = $.extend($.Event(type), {
  238. eventPhase: 2,
  239. currentTarget: this,
  240. srcElement: this,
  241. target: this,
  242. bubbles: false,
  243. cancelable: false
  244. }, props),
  245. applyArgs = [event];
  246. // Triggers local event handlers
  247. if (this.options[type].length) {
  248. for (var fn, i = 0; fn = this.options[type][i]; i++) {
  249. fn.apply(this.options.context, applyArgs);
  250. }
  251. }
  252. // Triggers global event handlers
  253. $.event.trigger("stream" + type.substring(0, 1).toUpperCase() + type.substring(1), applyArgs);
  254. },
  255. openURL: function() {
  256. var rts = /([?&]_=)[^&]*/;
  257. // Attaches a time stamp
  258. return (rts.test(this.url) ? this.url : (this.url + (/\?/.test(this.url) ? "&" : "?") + "_="))
  259. .replace(rts, "$1" + new Date().getTime());
  260. }
  261. });
  262. $.extend(Stream, {
  263. instances: {},
  264. // Prototype according to transport
  265. transports: {
  266. // XMLHttpRequest: Modern browsers except IE
  267. xhr: {
  268. open: function() {
  269. var self = this;
  270. this.xhr = new window.XMLHttpRequest();
  271. this.xhr.onreadystatechange = function() {
  272. switch (this.readyState) {
  273. case 2:
  274. try {
  275. $.noop(this.status);
  276. } catch (e) {
  277. // Opera throws an exception when accessing status property in LOADED state
  278. this.opera = true;
  279. }
  280. break;
  281. // Handles open and message event
  282. case 3:
  283. if (this.status !== 200) {
  284. return;
  285. }
  286. self.handleResponse(this.responseText);
  287. // For Opera
  288. if (this.opera && !this.polling) {
  289. this.polling = true;
  290. iterate(this, function() {
  291. if (this.readyState === 4) {
  292. return false;
  293. }
  294. if (this.responseText.length > self.message.index) {
  295. self.handleResponse(this.responseText);
  296. }
  297. });
  298. }
  299. break;
  300. // Handles error or close event
  301. case 4:
  302. self.handleClose(this.status !== 200);
  303. break;
  304. }
  305. };
  306. this.xhr.open("GET", this.openURL());
  307. this.xhr.send();
  308. },
  309. abort: function() {
  310. this.xhr.abort();
  311. }
  312. },
  313. // XDomainRequest: IE9, IE8
  314. xdr: {
  315. open: function() {
  316. var self = this;
  317. this.xdr = new window.XDomainRequest();
  318. // Handles open and message event
  319. this.xdr.onprogress = function() {
  320. self.handleResponse(this.responseText);
  321. };
  322. // Handles error event
  323. this.xdr.onerror = function() {
  324. self.handleClose(true);
  325. };
  326. // Handles close event
  327. this.xdr.onload = function() {
  328. self.handleClose();
  329. };
  330. this.xdr.open("GET", this.openURL());
  331. this.xdr.send();
  332. },
  333. abort: function() {
  334. var onload = this.xdr.onload;
  335. this.xdr.abort();
  336. onload();
  337. }
  338. },
  339. // Hidden iframe: IE7, IE6
  340. iframe: {
  341. open: function() {
  342. this.doc = new window.ActiveXObject("htmlfile");
  343. this.doc.open();
  344. this.doc.close();
  345. var iframe = this.doc.createElement("iframe");
  346. iframe.src = this.openURL();
  347. this.doc.body.appendChild(iframe);
  348. // For the server to respond in a consistent format regardless of user agent, we polls response text
  349. var cdoc = iframe.contentDocument || iframe.contentWindow.document;
  350. iterate(this, function() {
  351. var html = cdoc.documentElement;
  352. if (!html) {
  353. return;
  354. }
  355. // Detects connection failure
  356. if (cdoc.readyState === "complete") {
  357. try {
  358. $.noop(cdoc.fileSize);
  359. } catch(e) {
  360. this.handleClose(true);
  361. return false;
  362. }
  363. }
  364. var response = cdoc.body.firstChild;
  365. // Handles open event
  366. this.handleResponse(response.innerText);
  367. // Handles message and close event
  368. iterate(this, function() {
  369. var text = response.innerText;
  370. if (text.length > this.message.index) {
  371. this.handleResponse(text);
  372. // Empties response every time that it is handled
  373. response.innerText = "";
  374. this.message.index = 0;
  375. }
  376. if (cdoc.readyState === "complete") {
  377. this.handleClose();
  378. return false;
  379. }
  380. });
  381. return false;
  382. });
  383. },
  384. abort: function() {
  385. this.doc.execCommand("Stop");
  386. this.doc = null;
  387. }
  388. }
  389. }
  390. });
  391. // Detects Comet Streaming transport
  392. var transport = window.XDomainRequest ? "xdr" : window.ActiveXObject ? "iframe" : window.XMLHttpRequest ? "xhr" : null;
  393. if (!transport) {
  394. $.error("Unsupported browser");
  395. }
  396. // Completes the prototype
  397. $.extend(true, Stream.prototype, Stream.transports[transport]);
  398. // In case of reconnection, continues to communicate
  399. $(document).bind("streamOpen", function(e, event) {
  400. event.target.send();
  401. });
  402. // Closes all stream when the document is unloaded
  403. // this works right only in IE
  404. $(window).unload(function() {
  405. $.each(Stream.instances, function() {
  406. this.close();
  407. });
  408. });
  409. function iterate(context, fn) {
  410. (function loop() {
  411. setTimeout(function() {
  412. if (fn.call(context) === false) {
  413. return;
  414. }
  415. loop();
  416. }, 0);
  417. })();
  418. }
  419. $.stream = function(url, options) {
  420. if (!arguments.length) {
  421. for (var i in Stream.instances) {
  422. return Stream.instances[i];
  423. }
  424. return null;
  425. }
  426. return Stream.instances[url] || new Stream(url, options);
  427. };
  428. $.stream.version = "@VERSION";
  429. $.each("streamOpen streamMessage streamError streamClose".split(" "), function(i, o) {
  430. $.fn[o] = function(f) {
  431. return this.bind(o, f);
  432. };
  433. });
  434. })(jQuery);