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