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