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