PageRenderTime 271ms CodeModel.GetById 107ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 1ms

/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
 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);