PageRenderTime 70ms CodeModel.GetById 13ms app.highlight 51ms RepoModel.GetById 2ms app.codeStats 0ms

/tags/1.0/src/main/webapp/js/jquery.stream.js

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