PageRenderTime 50ms CodeModel.GetById 18ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/sub-projects/jquery-stream-jetty/trunk/src/main/webapp/jquery.stream-1.2.js

http://jquery-stream.googlecode.com/
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);