/tags/1.3b1/src/main/webapp/jquery.stream.js

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