/sub-projects/jquery-stream-jetty/trunk/src/main/webapp/jquery.stream-1.2.js

http://jquery-stream.googlecode.com/ · JavaScript · 763 lines · 511 code · 122 blank · 130 comment · 89 complexity · 572963f882b5a82fa526094bad48516a MD5 · raw file

  1. /*
  2. * jQuery Stream 1.2
  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.5+
  11. */
  12. (function($, undefined) {
  13. var // Stream object instances
  14. instances = {},
  15. // Streaming agents
  16. agents = {},
  17. // HTTP Streaming transports
  18. transports = {},
  19. // Does the throbber of doom exist?
  20. throbber = $.browser.webkit && !$.isReady;
  21. // Once the window is fully loaded, the throbber of doom will not be appearing
  22. if (throbber) {
  23. $(window).load(function() {
  24. throbber = false;
  25. });
  26. }
  27. // Stream is based on The WebSocket API
  28. // W3C Working Draft 19 April 2011 - http://www.w3.org/TR/2011/WD-websockets-20110419/
  29. $.stream = function(url, options) {
  30. // Returns the first Stream in the document
  31. if (!arguments.length) {
  32. for (var i in instances) {
  33. return instances[i];
  34. }
  35. return null;
  36. }
  37. // Stream to which the specified url or alias is mapped
  38. var instance = instances[url];
  39. if (!options) {
  40. return instance || null;
  41. } else if (instance && instance.readyState < 3) {
  42. return instance;
  43. }
  44. var // Stream object
  45. stream = {
  46. // URL to which to connect
  47. url: url,
  48. // Merges options
  49. options: $.stream.setup({}, options),
  50. // The state of stream
  51. // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
  52. readyState: 0,
  53. // Fake send
  54. send: function() {},
  55. // Fake close
  56. close: function() {}
  57. },
  58. match = /^(http|ws)s?:/.exec(stream.url),
  59. open = function() {
  60. // Delegates open process
  61. agents[stream.options.type](stream);
  62. };
  63. // Stream type
  64. if (match) {
  65. stream.options.type = match[1];
  66. }
  67. // Makes arrays of event handlers
  68. for (var i in {open: 1, message: 1, error: 1, close: 1}) {
  69. stream.options[i] = $.makeArray(stream.options[i]);
  70. }
  71. // The url and alias are a identifier of this instance within the document
  72. instances[stream.url] = stream;
  73. if (stream.options.alias) {
  74. instances[stream.options.alias] = stream;
  75. }
  76. // Deals with the throbber of doom
  77. if (stream.options.type === "ws" || !throbber) {
  78. open();
  79. } else {
  80. switch (stream.options.throbber.type || stream.options.throbber) {
  81. case "lazy":
  82. $(window).load(function() {
  83. setTimeout(open, stream.options.throbber.delay || 50);
  84. });
  85. break;
  86. case "reconnect":
  87. open();
  88. $(window).load(function() {
  89. if (stream.readyState === 0) {
  90. stream.options.open.push(function() {
  91. stream.options.open.pop();
  92. setTimeout(reconnect, 10);
  93. });
  94. } else {
  95. reconnect();
  96. }
  97. function reconnect() {
  98. stream.options.close.push(function() {
  99. stream.options.close.pop();
  100. setTimeout(function() {
  101. $.stream(stream.url, stream.options);
  102. }, stream.options.throbber.delay || 50);
  103. });
  104. var reconn = stream.options.reconnect;
  105. stream.close();
  106. stream.options.reconnect = reconn;
  107. }
  108. });
  109. break;
  110. }
  111. }
  112. return stream;
  113. };
  114. $.extend($.stream, {
  115. version: "1.2",
  116. // Logic borrowed from jQuery.ajaxSetup
  117. setup: function(target, options) {
  118. if (!options) {
  119. options = target;
  120. target = $.extend(true, $.stream.options, options);
  121. } else {
  122. $.extend(true, target, $.stream.options, options);
  123. }
  124. for (var field in {context: 1, url: 1}) {
  125. if (field in options) {
  126. target[field] = options[field];
  127. } else if (field in $.stream.options) {
  128. target[field] = $.stream.options[field];
  129. }
  130. }
  131. return target;
  132. },
  133. options: {
  134. // Stream type
  135. type: window.WebSocket ? "ws" : "http",
  136. // Whether to automatically reconnect when stream closed
  137. reconnect: true,
  138. // Whether to trigger global stream event handlers
  139. global: true,
  140. // Only for WebKit
  141. throbber: "lazy",
  142. // Message data type
  143. dataType: "text",
  144. // Message data converters
  145. converters: {
  146. text: window.String,
  147. json: $.parseJSON,
  148. xml: $.parseXML
  149. }
  150. // Additional parameters for GET request
  151. // openData: null,
  152. // WebSocket constructor argument
  153. // protocols: null,
  154. // XDomainRequest transport
  155. // enableXDR: false,
  156. // rewriteURL: null
  157. // Polling interval
  158. // operaInterval: 0
  159. // iframeInterval: 0
  160. }
  161. });
  162. $.extend(agents, {
  163. // WebSocket wrapper
  164. ws: function(stream) {
  165. if (!window.WebSocket) {
  166. return;
  167. }
  168. var // Absolute WebSocket URL
  169. url = prepareURL(getAbsoluteURL(stream.url).replace(/^http/, "ws"), stream.options.openData),
  170. // WebSocket instance
  171. ws = stream.options.protocols ? new window.WebSocket(url, stream.options.protocols) : new window.WebSocket(url);
  172. // WebSocket event handlers
  173. $.extend(ws, {
  174. onopen: function(event) {
  175. stream.readyState = 1;
  176. trigger(stream, event);
  177. },
  178. onmessage: function(event) {
  179. trigger(stream, $.extend({}, event, {data: stream.options.converters[stream.options.dataType](event.data)}));
  180. },
  181. onerror: function(event) {
  182. stream.options.reconnect = false;
  183. trigger(stream, event);
  184. },
  185. onclose: function(event) {
  186. var readyState = stream.readyState;
  187. stream.readyState = 3;
  188. trigger(stream, event);
  189. // Reconnect?
  190. if (stream.options.reconnect && readyState !== 0) {
  191. $.stream(stream.url, stream.options);
  192. }
  193. }
  194. });
  195. // Overrides send and close
  196. $.extend(stream, {
  197. send: function(data) {
  198. if (stream.readyState === 0) {
  199. $.error("INVALID_STATE_ERR: Stream not open");
  200. }
  201. ws.send(typeof data === "string" ? data : param(data));
  202. },
  203. close: function() {
  204. if (stream.readyState < 2) {
  205. stream.readyState = 2;
  206. stream.options.reconnect = false;
  207. ws.close();
  208. }
  209. }
  210. });
  211. },
  212. // HTTP Streaming
  213. http: function(stream) {
  214. var // Transport
  215. transportFn,
  216. transport,
  217. // Low-level request and response handler
  218. handleOpen,
  219. handleMessage,
  220. handleSend,
  221. // Latch for AJAX
  222. sending,
  223. // Data queue
  224. dataQueue = [],
  225. // Helper object for parsing response
  226. message = {
  227. // The index from which to start parsing
  228. index: 0,
  229. // The temporary data
  230. data: ""
  231. };
  232. // Chooses a proper transport
  233. transportFn = transports[
  234. // xdr
  235. stream.options.enableXDR && window.XDomainRequest ? "xdr" :
  236. // iframe
  237. window.ActiveXObject ? "iframe" :
  238. // xhr
  239. window.XMLHttpRequest ? "xhr" : null];
  240. if (!transportFn) {
  241. return;
  242. }
  243. // Default response handler
  244. handleOpen = stream.options.handleOpen || function(text, message, stream) {
  245. // The top of the response is made up of the id and padding
  246. // optional identifier within the server
  247. stream.id = text.substring(0, text.indexOf(";"));
  248. // message.index = text.indexOf(";", stream.id.length + ";".length) + ";".length;
  249. message.index = text.indexOf(";", stream.id.length + 1) + 1;
  250. };
  251. handleMessage = stream.options.handleMessage || function(text, message) {
  252. // Response could contain a single message, multiple messages or a fragment of a message
  253. // default message format is message-size ; message-data ;
  254. if (message.size == null) {
  255. // Checks a semicolon of size part
  256. var sizeEnd = text.indexOf(";", message.index);
  257. if (sizeEnd < 0) {
  258. return false;
  259. }
  260. message.size = +text.substring(message.index, sizeEnd);
  261. // index: sizeEnd + ";".length,
  262. message.index = sizeEnd + 1;
  263. }
  264. var data = text.substr(message.index, message.size - message.data.length);
  265. message.data += data;
  266. message.index += data.length;
  267. // Has stream message been completed?
  268. if (message.size !== message.data.length) {
  269. return false;
  270. }
  271. // Checks a semicolon of data part
  272. var dataEnd = text.indexOf(";", message.index);
  273. if (dataEnd < 0) {
  274. return false;
  275. }
  276. // message.index = dataEnd + ";".length;
  277. message.index = dataEnd + 1;
  278. // Completes parsing
  279. delete message.size;
  280. };
  281. // Default request handler
  282. handleSend = stream.options.handleSend || function(type, options, stream) {
  283. var metadata = {"metadata.id": stream.id, "metadata.type": type};
  284. options.data =
  285. // Close
  286. type === "close" ? param(metadata) :
  287. // Send
  288. // converts data if not already a string
  289. ((typeof options.data === "string" ? options.data : param(options.data)) + "&" + param(metadata));
  290. };
  291. transport = transportFn(stream, {
  292. response: function(text) {
  293. if (stream.readyState === 0) {
  294. if (handleOpen(text, message, stream) === false) {
  295. return;
  296. }
  297. stream.readyState = 1;
  298. trigger(stream, "open");
  299. }
  300. for (;;) {
  301. if (handleMessage(text, message, stream) === false) {
  302. return;
  303. }
  304. if (stream.readyState < 3) {
  305. // Pseudo MessageEvent
  306. trigger(stream, "message", {
  307. // Converts the data type
  308. data: stream.options.converters[stream.options.dataType](message.data),
  309. origin: "",
  310. lastEventId: "",
  311. source: null,
  312. ports: null
  313. });
  314. }
  315. // Resets the data
  316. message.data = "";
  317. }
  318. },
  319. close: function(isError) {
  320. var readyState = stream.readyState;
  321. stream.readyState = 3;
  322. if (isError) {
  323. // Prevents reconnecting
  324. stream.options.reconnect = false;
  325. // If establishing a connection fails, fires the close event instead of the error event
  326. if (readyState === 0) {
  327. // Pseudo CloseEvent
  328. trigger(stream, "close", {
  329. wasClean: false,
  330. code: null,
  331. reason: ""
  332. });
  333. } else {
  334. trigger(stream, "error");
  335. }
  336. } else {
  337. // Pseudo CloseEvent
  338. trigger(stream, "close", {
  339. // Presumes that the stream closed cleanly
  340. wasClean: true,
  341. code: null,
  342. reason: ""
  343. });
  344. // Reconnect?
  345. if (stream.options.reconnect) {
  346. $.stream(stream.url, stream.options);
  347. }
  348. }
  349. }
  350. }, message);
  351. transport.open();
  352. // Overrides send and close
  353. $.extend(stream, {
  354. send: function(data) {
  355. if (stream.readyState === 0) {
  356. $.error("INVALID_STATE_ERR: Stream not open");
  357. }
  358. // Pushes the data into the queue
  359. dataQueue.push(data);
  360. if (!sending) {
  361. sending = true;
  362. // Performs an Ajax iterating through the data queue
  363. (function post() {
  364. if (stream.readyState === 1 && dataQueue.length) {
  365. var options = {url: stream.url, type: "POST", data: dataQueue.shift()};
  366. if (handleSend("send", options, stream) !== false) {
  367. $.ajax(options).complete(post);
  368. } else {
  369. post();
  370. }
  371. } else {
  372. sending = false;
  373. }
  374. })();
  375. }
  376. },
  377. close: function() {
  378. // Do nothing if the readyState is in the CLOSING or CLOSED
  379. if (stream.readyState < 2) {
  380. stream.readyState = 2;
  381. var options = {url: stream.url, type: "POST"};
  382. if (handleSend("close", options, stream) !== false) {
  383. // Notifies the server
  384. $.ajax(options);
  385. }
  386. // Prevents reconnecting
  387. stream.options.reconnect = false;
  388. transport.close();
  389. }
  390. }
  391. });
  392. }
  393. });
  394. $.extend(transports, {
  395. // XMLHttpRequest: Modern browsers except Internet Explorer
  396. xhr: function(stream, handler, message) {
  397. var stop,
  398. polling,
  399. preStatus,
  400. xhr = new window.XMLHttpRequest();
  401. xhr.onreadystatechange = function() {
  402. switch (xhr.readyState) {
  403. // Handles open and message event
  404. case 3:
  405. if (xhr.status !== 200) {
  406. return;
  407. }
  408. handler.response(xhr.responseText);
  409. // For Opera
  410. if ($.browser.opera && !polling) {
  411. polling = true;
  412. stop = iterate(function() {
  413. if (xhr.readyState === 4) {
  414. return false;
  415. }
  416. if (xhr.responseText.length > message.index) {
  417. handler.response(xhr.responseText);
  418. }
  419. }, stream.options.operaInterval);
  420. }
  421. break;
  422. // Handles error or close event
  423. case 4:
  424. // HTTP status 0 could mean that the request is terminated by abort method
  425. // but it's not error in Stream object
  426. handler.close(xhr.status !== 200 && preStatus !== 200);
  427. break;
  428. }
  429. };
  430. return {
  431. open: function() {
  432. xhr.open("GET", prepareURL(stream.url, stream.options.openData));
  433. xhr.send();
  434. },
  435. close: function() {
  436. if (stop) {
  437. stop();
  438. }
  439. // Saves status
  440. try {
  441. preStatus = xhr.status;
  442. } catch (e) {}
  443. xhr.abort();
  444. }
  445. };
  446. },
  447. // Hidden iframe: Internet Explorer
  448. iframe: function(stream, handler, message) {
  449. var stop,
  450. closed,
  451. onload = function() {
  452. if (!closed) {
  453. closed = true;
  454. handler.close();
  455. }
  456. },
  457. doc = new window.ActiveXObject("htmlfile");
  458. doc.open();
  459. doc.close();
  460. return {
  461. open: function() {
  462. var iframe = doc.createElement("iframe");
  463. iframe.src = prepareURL(stream.url, stream.options.openData);
  464. doc.body.appendChild(iframe);
  465. // For the server to respond in a consistent format regardless of user agent, we polls response text
  466. var cdoc = iframe.contentDocument || iframe.contentWindow.document;
  467. stop = iterate(function() {
  468. if (!cdoc.documentElement) {
  469. return;
  470. }
  471. // Detects connection failure
  472. if (cdoc.readyState === "complete") {
  473. try {
  474. $.noop(cdoc.fileSize);
  475. } catch(e) {
  476. handler.close(true);
  477. return false;
  478. }
  479. }
  480. var response = cdoc.body.lastChild,
  481. readResponse = function() {
  482. // Clones the element not to disturb the original one
  483. var clone = response.cloneNode(true);
  484. // If the last character is a carriage return or a line feed, IE ignores it in the innerText property
  485. // therefore, we add another non-newline character to preserve it
  486. clone.appendChild(cdoc.createTextNode("."));
  487. var text = clone.innerText;
  488. return text.substring(0, text.length - 1);
  489. };
  490. // To support text/html content type
  491. if (!$.nodeName(response, "pre")) {
  492. // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped
  493. // it is deprecated in HTML5, but still works
  494. var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement,
  495. script = cdoc.createElement("script");
  496. script.text = "document.write('<plaintext>')";
  497. head.insertBefore(script, head.firstChild);
  498. head.removeChild(script);
  499. // The plaintext element will be the response container
  500. response = cdoc.body.lastChild;
  501. }
  502. // Handles open event
  503. handler.response(readResponse());
  504. // Handles message and close event
  505. stop = iterate(function() {
  506. var text = readResponse();
  507. if (text.length > message.index) {
  508. handler.response(text);
  509. // Empties response every time that it is handled
  510. response.innerText = "";
  511. message.index = 0;
  512. }
  513. if (cdoc.readyState === "complete") {
  514. onload();
  515. return false;
  516. }
  517. }, stream.options.iframeInterval);
  518. return false;
  519. });
  520. },
  521. close: function() {
  522. if (stop) {
  523. stop();
  524. }
  525. doc.execCommand("Stop");
  526. onload();
  527. }
  528. };
  529. },
  530. // XDomainRequest: Optionally Internet Explorer 8+
  531. xdr: function(stream, handler) {
  532. var xdr = new window.XDomainRequest(),
  533. rewriteURL = stream.options.rewriteURL || function(url) {
  534. // Maintaining session by rewriting URL
  535. // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
  536. var rewriters = {
  537. JSESSIONID: function(sid) {
  538. return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1");
  539. },
  540. PHPSESSID: function(sid) {
  541. return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, "");
  542. }
  543. };
  544. for (var name in rewriters) {
  545. // Finds session id from cookie
  546. var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie);
  547. if (matcher) {
  548. return rewriters[name](matcher[1]);
  549. }
  550. }
  551. return url;
  552. };
  553. // Handles open and message event
  554. xdr.onprogress = function() {
  555. handler.response(xdr.responseText);
  556. };
  557. // Handles error event
  558. xdr.onerror = function() {
  559. handler.close(true);
  560. };
  561. // Handles close event
  562. var onload = xdr.onload = function() {
  563. handler.close();
  564. };
  565. return {
  566. open: function() {
  567. xdr.open("GET", prepareURL(rewriteURL(stream.url), stream.options.openData));
  568. xdr.send();
  569. },
  570. close: function() {
  571. xdr.abort();
  572. onload();
  573. }
  574. };
  575. }
  576. });
  577. // Closes all stream when the document is unloaded
  578. // this works right only in IE
  579. $(window).bind("unload.stream", function() {
  580. for (var url in instances) {
  581. instances[url].close();
  582. delete instances[url];
  583. }
  584. });
  585. $.each("streamOpen streamMessage streamError streamClose".split(" "), function(i, o) {
  586. $.fn[o] = function(f) {
  587. return this.bind(o, f);
  588. };
  589. });
  590. // Works even in IE6
  591. function getAbsoluteURL(url) {
  592. var div = document.createElement("div");
  593. div.innerHTML = "<a href='" + url + "'/>";
  594. return div.firstChild.href;
  595. }
  596. function trigger(stream, event, props) {
  597. event = event.type ?
  598. event :
  599. $.extend($.Event(event), {bubbles: false, cancelable: false}, props);
  600. var handlers = stream.options[event.type],
  601. applyArgs = [event, stream];
  602. // Triggers local event handlers
  603. for (var i = 0, length = handlers.length; i < length; i++) {
  604. handlers[i].apply(stream.options.context, applyArgs);
  605. }
  606. if (stream.options.global) {
  607. // Triggers global event handlers
  608. $.event.trigger("stream" + event.type.substring(0, 1).toUpperCase() + event.type.substring(1), applyArgs);
  609. }
  610. }
  611. function prepareURL(url, data) {
  612. // Converts data into a query string
  613. if (data && typeof data !== "string") {
  614. data = param(data);
  615. }
  616. // Attaches a time stamp to prevent caching
  617. var ts = $.now(),
  618. ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts);
  619. return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : "") + (data ? ("&" + data) : "");
  620. }
  621. function param(data) {
  622. return $.param(data, $.ajaxSettings.traditional);
  623. }
  624. function iterate(fn, interval) {
  625. var timeoutId;
  626. // Though the interval is 0 for real-time application, there is a delay between setTimeout calls
  627. // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
  628. interval = interval || 0;
  629. (function loop() {
  630. timeoutId = setTimeout(function() {
  631. if (fn() === false) {
  632. return;
  633. }
  634. loop();
  635. }, interval);
  636. })();
  637. return function() {
  638. clearTimeout(timeoutId);
  639. };
  640. }
  641. })(jQuery);