diff --git a/build/gallery-eventsource/gallery-eventsource-debug.js b/build/gallery-eventsource/gallery-eventsource-debug.js new file mode 100644 index 0000000000..e3e0803158 --- /dev/null +++ b/build/gallery-eventsource/gallery-eventsource-debug.js @@ -0,0 +1,274 @@ +YUI.add('gallery-eventsource', function(Y) { + + +/*global EventSource*/ + + var useNative = typeof EventSource != "undefined", + YUIEvenSourceProto; + + function YUIEventSource(url){ + + Y.Event.Target.call(this); + + /** + * The URL or the server-sent events. + * @type String + * @property url + */ + this.url = url; + + + /** + * The current state of the object. Possible values are 0 for connecting, + * 1 for connected, and 2 for disconnected. + * @type int + * @property readyState + */ + this.readyState = 0; + + /** + * Object used to communicate with the server. May be an XHR or an + * EventSource. + * @type XMLHttpRequest|EventSource + * @property _transport + * @private + */ + this._transport = null; + + //initialize the object + this._init(); + + } + + var YUIEventSourceProto; + + //use native if available + if (useNative){ + YUIEventSourceProto = { + + _init: function(){ + var src = new EventSource(this.url), + that = this; + + //map events to custom events + src.onopen = function(event){ + that.fire("open"); + that.readyState = 1; + }; + src.onmessage = function(event){ + that.fire({type: "message", data: event.data}); + }; + src.onerror = function(event){ + that.fire("error"); + that.readyState = 2; + }; + + this._transport = src; + }, + + close: function(){ + this._transport.close(); + that.readyState = 2; + } + + }; + + } else { + + YUIEventSourceProto = { + + /** + * Initializes the EventSource object. Either creates an EventSource + * instance or an XHR to mimic the functionality. + * @method _init + * @return {void} + * @private + */ + _init: function(){ + var src, + that = this; + + /** + * Keeps track of where in the response buffer to start + * evaluating new data. Only used when native EventSource + * is not available. + * @type int + * @property _lastIndex + * @private + */ + this._lastIndex = 0; + + //use appropriate XHR object as transport + if (typeof XMLHttpRequest != "undefined"){ //most browsers + src = new XMLHttpRequest(); + } else if (typeof ActiveXObject != "undefined"){ //IE6 + src = new ActiveXObject("MSXML2.XMLHttp"); + } + + src.open("get", this.url, true); + + /* + * IE < 8 will not have multiple readyState 3 calls, so + * those will go to readyState 4 and effectively become + * long-polling requests. All others will have a hanging + * GET request that receives continual information over + * the same connection. + */ + src.onreadystatechange = function(){ + if (src.readyState == 3){ + that._updateReadyState(); + + //IE6 and IE7 throw an error when trying to access responseText here + try { + that._processIncomingData(src.responseText); + } catch(ex){ + //noop + } + } else if (src.readyState == 4){ + that._updateReadyState(); + that._validateResponse(); + } + }; + + this._transport = src; + + //wait until this JS task is done before firing + //so as not to lose any events + setTimeout(function(){ + src.send(null); + }, 0); + }, + + /** + * Called when XHR readyState 4 occurs. Processes the response, + * then reopens the connection to the server unless close() + * has been called. + * @method _validateResponse + * @return {void} + * @private + */ + _validateResponse: function(){ + var src = this._transport; + if (src.status >= 200 && src.status < 300){ + this._processIncomingData(src.responseText); + + //readyState will be 2 if close() was called + if (this.readyState != 2){ + this._transport.onreadystatechange = function(){}; + this._init(); + } + } else { + this.fire("error"); + this.readyState = 2; + } + + //prevent memory leaks due to closure + src = null; + }, + + /** + * Updates the readyState property to 1 if it's still + * set at 0. + * @method _updateReadyState + * @return {void} + * @private + */ + _updateReadyState: function(){ + if (this.readyState == 0){ + this.readyState == 1; + } + }, + + /** + * Processes the data stream as server-sent events. Goes line by + * line looking for event information and fires the message + * event where appropriate. + * @param {String} text The text to parse. + * @return {void} + * @private + * @method _processIncomingData + */ + _processIncomingData: function(text){ + text = text.substring(this._lastIndex); + this._lastIndex += text.length; + + var lines = text.split("\n"), + parts, + i = 0, + len = lines.length, + data = "", + eventName = ""; + + while (i < len){ + + if (lines[i].indexOf(":") > -1){ + + parts = lines[i].split(":"); + + //shift off the first item to check the value + //keep in mind that "data: a:b" is a valid value + switch(parts.shift()){ + case "data": + data += parts.join(":") + "\n"; + break; + + case "event": + eventName = parts[1]; + break; + + case "id": + //todo + break; + } + + } else if (lines[i].replace(/\s/g, "") == ""){ + //an empty line means to flush the event buffer of data + //but only if there's data to send + + if (data != ""){ + + //per spec, strip off last newline + if (data.charAt(data.length-1) == "\n"){ + data = data.substring(0,data.length-1); + } + + //an empty line means a message is complete + this.fire({type: "message", data: data}); + + //clear the existing data + data = ""; + eventName = ""; + } + + } + + i++; + } + + }, + + /** + * Permanently close the connection with the server. + * @method close + * @return {void} + */ + close: function(){ + this.readyState = 2; + this._transport.abort(); + } + + }; + + + + } + + //inherit from Event.Target to get events, and assign instance methods + Y.extend(YUIEventSource, Y.Event.Target, YUIEventSourceProto); + + //publish to Y object + Y.EventSource = YUIEventSource; + + + +}, '@VERSION@' ,{requires:['event-base','event-custom']}); diff --git a/build/gallery-eventsource/gallery-eventsource-min.js b/build/gallery-eventsource/gallery-eventsource-min.js new file mode 100644 index 0000000000..e8ab934e5b --- /dev/null +++ b/build/gallery-eventsource/gallery-eventsource-min.js @@ -0,0 +1 @@ +YUI.add("gallery-eventsource",function(E){var A=typeof EventSource!="undefined",C;function B(F){E.Event.Target.call(this);this.url=F;this.readyState=0;this._transport=null;this._init();}var D;if(A){D={_init:function(){var G=new EventSource(this.url),F=this;G.onopen=function(H){F.fire("open");F.readyState=1;};G.onmessage=function(H){F.fire({type:"message",data:H.data});};G.onerror=function(H){F.fire("error");F.readyState=2;};this._transport=G;},close:function(){this._transport.close();that.readyState=2;}};}else{D={_init:function(){var G,F=this;this._lastIndex=0;if(typeof XMLHttpRequest!="undefined"){G=new XMLHttpRequest();}else{if(typeof ActiveXObject!="undefined"){G=new ActiveXObject("MSXML2.XMLHttp");}}G.open("get",this.url,true);G.onreadystatechange=function(){if(G.readyState==3){F._updateReadyState();try{F._processIncomingData(G.responseText);}catch(H){}}else{if(G.readyState==4){F._updateReadyState();F._validateResponse();}}};this._transport=G;setTimeout(function(){G.send(null);},0);},_validateResponse:function(){var F=this._transport;if(F.status>=200&&F.status<300){this._processIncomingData(F.responseText);if(this.readyState!=2){this._transport.onreadystatechange=function(){};this._init();}}else{this.fire("error");this.readyState=2;}F=null;},_updateReadyState:function(){if(this.readyState==0){this.readyState==1;}},_processIncomingData:function(L){L=L.substring(this._lastIndex);this._lastIndex+=L.length;var G=L.split("\n"),K,I=0,F=G.length,J="",H="";while(I-1){K=G[I].split(":");switch(K.shift()){case"data":J+=K.join(":")+"\n";break;case"event":H=K[1];break;case"id":break;}}else{if(G[I].replace(/\s/g,"")==""){if(J!=""){if(J.charAt(J.length-1)=="\n"){J=J.substring(0,J.length-1);}this.fire({type:"message",data:J});J="";H="";}}}I++;}},close:function(){this.readyState=2;this._transport.abort();}};}E.extend(B,E.Event.Target,D);E.EventSource=B;},"@VERSION@",{requires:["event-base","event-custom"]}); \ No newline at end of file diff --git a/build/gallery-eventsource/gallery-eventsource.js b/build/gallery-eventsource/gallery-eventsource.js new file mode 100644 index 0000000000..e3e0803158 --- /dev/null +++ b/build/gallery-eventsource/gallery-eventsource.js @@ -0,0 +1,274 @@ +YUI.add('gallery-eventsource', function(Y) { + + +/*global EventSource*/ + + var useNative = typeof EventSource != "undefined", + YUIEvenSourceProto; + + function YUIEventSource(url){ + + Y.Event.Target.call(this); + + /** + * The URL or the server-sent events. + * @type String + * @property url + */ + this.url = url; + + + /** + * The current state of the object. Possible values are 0 for connecting, + * 1 for connected, and 2 for disconnected. + * @type int + * @property readyState + */ + this.readyState = 0; + + /** + * Object used to communicate with the server. May be an XHR or an + * EventSource. + * @type XMLHttpRequest|EventSource + * @property _transport + * @private + */ + this._transport = null; + + //initialize the object + this._init(); + + } + + var YUIEventSourceProto; + + //use native if available + if (useNative){ + YUIEventSourceProto = { + + _init: function(){ + var src = new EventSource(this.url), + that = this; + + //map events to custom events + src.onopen = function(event){ + that.fire("open"); + that.readyState = 1; + }; + src.onmessage = function(event){ + that.fire({type: "message", data: event.data}); + }; + src.onerror = function(event){ + that.fire("error"); + that.readyState = 2; + }; + + this._transport = src; + }, + + close: function(){ + this._transport.close(); + that.readyState = 2; + } + + }; + + } else { + + YUIEventSourceProto = { + + /** + * Initializes the EventSource object. Either creates an EventSource + * instance or an XHR to mimic the functionality. + * @method _init + * @return {void} + * @private + */ + _init: function(){ + var src, + that = this; + + /** + * Keeps track of where in the response buffer to start + * evaluating new data. Only used when native EventSource + * is not available. + * @type int + * @property _lastIndex + * @private + */ + this._lastIndex = 0; + + //use appropriate XHR object as transport + if (typeof XMLHttpRequest != "undefined"){ //most browsers + src = new XMLHttpRequest(); + } else if (typeof ActiveXObject != "undefined"){ //IE6 + src = new ActiveXObject("MSXML2.XMLHttp"); + } + + src.open("get", this.url, true); + + /* + * IE < 8 will not have multiple readyState 3 calls, so + * those will go to readyState 4 and effectively become + * long-polling requests. All others will have a hanging + * GET request that receives continual information over + * the same connection. + */ + src.onreadystatechange = function(){ + if (src.readyState == 3){ + that._updateReadyState(); + + //IE6 and IE7 throw an error when trying to access responseText here + try { + that._processIncomingData(src.responseText); + } catch(ex){ + //noop + } + } else if (src.readyState == 4){ + that._updateReadyState(); + that._validateResponse(); + } + }; + + this._transport = src; + + //wait until this JS task is done before firing + //so as not to lose any events + setTimeout(function(){ + src.send(null); + }, 0); + }, + + /** + * Called when XHR readyState 4 occurs. Processes the response, + * then reopens the connection to the server unless close() + * has been called. + * @method _validateResponse + * @return {void} + * @private + */ + _validateResponse: function(){ + var src = this._transport; + if (src.status >= 200 && src.status < 300){ + this._processIncomingData(src.responseText); + + //readyState will be 2 if close() was called + if (this.readyState != 2){ + this._transport.onreadystatechange = function(){}; + this._init(); + } + } else { + this.fire("error"); + this.readyState = 2; + } + + //prevent memory leaks due to closure + src = null; + }, + + /** + * Updates the readyState property to 1 if it's still + * set at 0. + * @method _updateReadyState + * @return {void} + * @private + */ + _updateReadyState: function(){ + if (this.readyState == 0){ + this.readyState == 1; + } + }, + + /** + * Processes the data stream as server-sent events. Goes line by + * line looking for event information and fires the message + * event where appropriate. + * @param {String} text The text to parse. + * @return {void} + * @private + * @method _processIncomingData + */ + _processIncomingData: function(text){ + text = text.substring(this._lastIndex); + this._lastIndex += text.length; + + var lines = text.split("\n"), + parts, + i = 0, + len = lines.length, + data = "", + eventName = ""; + + while (i < len){ + + if (lines[i].indexOf(":") > -1){ + + parts = lines[i].split(":"); + + //shift off the first item to check the value + //keep in mind that "data: a:b" is a valid value + switch(parts.shift()){ + case "data": + data += parts.join(":") + "\n"; + break; + + case "event": + eventName = parts[1]; + break; + + case "id": + //todo + break; + } + + } else if (lines[i].replace(/\s/g, "") == ""){ + //an empty line means to flush the event buffer of data + //but only if there's data to send + + if (data != ""){ + + //per spec, strip off last newline + if (data.charAt(data.length-1) == "\n"){ + data = data.substring(0,data.length-1); + } + + //an empty line means a message is complete + this.fire({type: "message", data: data}); + + //clear the existing data + data = ""; + eventName = ""; + } + + } + + i++; + } + + }, + + /** + * Permanently close the connection with the server. + * @method close + * @return {void} + */ + close: function(){ + this.readyState = 2; + this._transport.abort(); + } + + }; + + + + } + + //inherit from Event.Target to get events, and assign instance methods + Y.extend(YUIEventSource, Y.Event.Target, YUIEventSourceProto); + + //publish to Y object + Y.EventSource = YUIEventSource; + + + +}, '@VERSION@' ,{requires:['event-base','event-custom']}); diff --git a/src/gallery-eventsource/build.properties b/src/gallery-eventsource/build.properties new file mode 100644 index 0000000000..c8d70b792e --- /dev/null +++ b/src/gallery-eventsource/build.properties @@ -0,0 +1,4 @@ +builddir=../../../builder/componentbuild +component=gallery-eventsource +component.jsfiles=eventsource.js +component.requires=event-base,event-custom diff --git a/src/gallery-eventsource/build.xml b/src/gallery-eventsource/build.xml new file mode 100644 index 0000000000..f74f58df66 --- /dev/null +++ b/src/gallery-eventsource/build.xml @@ -0,0 +1,7 @@ + + + + EventSource Build File + + + diff --git a/src/gallery-eventsource/js/eventsource.js b/src/gallery-eventsource/js/eventsource.js new file mode 100644 index 0000000000..80d0a513eb --- /dev/null +++ b/src/gallery-eventsource/js/eventsource.js @@ -0,0 +1,268 @@ + +/*global EventSource*/ + + var useNative = typeof EventSource != "undefined", + YUIEvenSourceProto; + + function YUIEventSource(url){ + + Y.Event.Target.call(this); + + /** + * The URL or the server-sent events. + * @type String + * @property url + */ + this.url = url; + + + /** + * The current state of the object. Possible values are 0 for connecting, + * 1 for connected, and 2 for disconnected. + * @type int + * @property readyState + */ + this.readyState = 0; + + /** + * Object used to communicate with the server. May be an XHR or an + * EventSource. + * @type XMLHttpRequest|EventSource + * @property _transport + * @private + */ + this._transport = null; + + //initialize the object + this._init(); + + } + + var YUIEventSourceProto; + + //use native if available + if (useNative){ + YUIEventSourceProto = { + + _init: function(){ + var src = new EventSource(this.url), + that = this; + + //map events to custom events + src.onopen = function(event){ + that.fire("open"); + that.readyState = 1; + }; + src.onmessage = function(event){ + that.fire({type: "message", data: event.data}); + }; + src.onerror = function(event){ + that.fire("error"); + that.readyState = 2; + }; + + this._transport = src; + }, + + close: function(){ + this._transport.close(); + that.readyState = 2; + } + + }; + + } else { + + YUIEventSourceProto = { + + /** + * Initializes the EventSource object. Either creates an EventSource + * instance or an XHR to mimic the functionality. + * @method _init + * @return {void} + * @private + */ + _init: function(){ + var src, + that = this; + + /** + * Keeps track of where in the response buffer to start + * evaluating new data. Only used when native EventSource + * is not available. + * @type int + * @property _lastIndex + * @private + */ + this._lastIndex = 0; + + //use appropriate XHR object as transport + if (typeof XMLHttpRequest != "undefined"){ //most browsers + src = new XMLHttpRequest(); + } else if (typeof ActiveXObject != "undefined"){ //IE6 + src = new ActiveXObject("MSXML2.XMLHttp"); + } + + src.open("get", this.url, true); + + /* + * IE < 8 will not have multiple readyState 3 calls, so + * those will go to readyState 4 and effectively become + * long-polling requests. All others will have a hanging + * GET request that receives continual information over + * the same connection. + */ + src.onreadystatechange = function(){ + if (src.readyState == 3){ + that._updateReadyState(); + + //IE6 and IE7 throw an error when trying to access responseText here + try { + that._processIncomingData(src.responseText); + } catch(ex){ + //noop + } + } else if (src.readyState == 4){ + that._updateReadyState(); + that._validateResponse(); + } + }; + + this._transport = src; + + //wait until this JS task is done before firing + //so as not to lose any events + setTimeout(function(){ + src.send(null); + }, 0); + }, + + /** + * Called when XHR readyState 4 occurs. Processes the response, + * then reopens the connection to the server unless close() + * has been called. + * @method _validateResponse + * @return {void} + * @private + */ + _validateResponse: function(){ + var src = this._transport; + if (src.status >= 200 && src.status < 300){ + this._processIncomingData(src.responseText); + + //readyState will be 2 if close() was called + if (this.readyState != 2){ + this._transport.onreadystatechange = function(){}; + this._init(); + } + } else { + this.fire("error"); + this.readyState = 2; + } + + //prevent memory leaks due to closure + src = null; + }, + + /** + * Updates the readyState property to 1 if it's still + * set at 0. + * @method _updateReadyState + * @return {void} + * @private + */ + _updateReadyState: function(){ + if (this.readyState == 0){ + this.readyState == 1; + } + }, + + /** + * Processes the data stream as server-sent events. Goes line by + * line looking for event information and fires the message + * event where appropriate. + * @param {String} text The text to parse. + * @return {void} + * @private + * @method _processIncomingData + */ + _processIncomingData: function(text){ + text = text.substring(this._lastIndex); + this._lastIndex += text.length; + + var lines = text.split("\n"), + parts, + i = 0, + len = lines.length, + data = "", + eventName = ""; + + while (i < len){ + + if (lines[i].indexOf(":") > -1){ + + parts = lines[i].split(":"); + + //shift off the first item to check the value + //keep in mind that "data: a:b" is a valid value + switch(parts.shift()){ + case "data": + data += parts.join(":") + "\n"; + break; + + case "event": + eventName = parts[1]; + break; + + case "id": + //todo + break; + } + + } else if (lines[i].replace(/\s/g, "") == ""){ + //an empty line means to flush the event buffer of data + //but only if there's data to send + + if (data != ""){ + + //per spec, strip off last newline + if (data.charAt(data.length-1) == "\n"){ + data = data.substring(0,data.length-1); + } + + //an empty line means a message is complete + this.fire({type: "message", data: data}); + + //clear the existing data + data = ""; + eventName = ""; + } + + } + + i++; + } + + }, + + /** + * Permanently close the connection with the server. + * @method close + * @return {void} + */ + close: function(){ + this.readyState = 2; + this._transport.abort(); + } + + }; + + + + } + + //inherit from Event.Target to get events, and assign instance methods + Y.extend(YUIEventSource, Y.Event.Target, YUIEventSourceProto); + + //publish to Y object + Y.EventSource = YUIEventSource; diff --git a/src/gallery-eventsource/tests/data.txt b/src/gallery-eventsource/tests/data.txt new file mode 100644 index 0000000000..5a4a85109a --- /dev/null +++ b/src/gallery-eventsource/tests/data.txt @@ -0,0 +1,11 @@ +data: foo + + + +data: bar + + + +data: foo +data: bar + diff --git a/src/gallery-eventsource/tests/eventsource.html b/src/gallery-eventsource/tests/eventsource.html new file mode 100644 index 0000000000..e16a9caa98 --- /dev/null +++ b/src/gallery-eventsource/tests/eventsource.html @@ -0,0 +1,60 @@ + + +EventSource Tests + + + +

EventSource Tests

+
+ + +