/* * EventSource polyfill version 0.9.6 * Supported by sc AmvTek srl * :email: devel@amvtek.com */ ;(function (global) { if (global.EventSource && !global._eventSourceImportPrefix){ return; } var evsImportName = (global._eventSourceImportPrefix||'')+"EventSource"; var EventSource = function (url, options) { if (!url || typeof url != 'string') { throw new SyntaxError('Not enough arguments'); } this.URL = url; this.setOptions(options); var evs = this; setTimeout(function(){evs.poll()}, 0); }; EventSource.prototype = { CONNECTING: 0, OPEN: 1, CLOSED: 2, defaultOptions: { loggingEnabled: false, loggingPrefix: "eventsource", interval: 500, // milliseconds bufferSizeLimit: 256*1024, // bytes silentTimeout: 300000, // milliseconds getArgs:{ 'evs_buffer_size_limit': 256*1024 }, xhrHeaders:{ 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', 'X-Requested-With': 'XMLHttpRequest' } }, setOptions: function(options){ var defaults = this.defaultOptions; var option; // set all default options... for (option in defaults){ if ( defaults.hasOwnProperty(option) ){ this[option] = defaults[option]; } } // override with what is in options for (option in options){ if (option in defaults && options.hasOwnProperty(option)){ this[option] = options[option]; } } // if getArgs option is enabled // ensure evs_buffer_size_limit corresponds to bufferSizeLimit if (this.getArgs && this.bufferSizeLimit) { this.getArgs['evs_buffer_size_limit'] = this.bufferSizeLimit; } // if console is not available, force loggingEnabled to false if (typeof console === "undefined" || typeof console.log === "undefined") { this.loggingEnabled = false; } }, log: function(message) { if (this.loggingEnabled) { console.log("[" + this.loggingPrefix +"]:" + message) } }, poll: function() { try { if (this.readyState == this.CLOSED) { return; } this.cleanup(); this.readyState = this.CONNECTING; this.cursor = 0; this.cache = ''; this._xhr = new this.XHR(this); this.resetNoActivityTimer(); } catch (e) { // in an attempt to silence the errors this.log('There were errors inside the pool try-catch'); this.dispatchEvent('error', { type: 'error', data: e.message }); } }, pollAgain: function (interval) { // schedule poll to be called after interval milliseconds var evs = this; evs.readyState = evs.CONNECTING; evs.dispatchEvent('error', { type: 'error', data: "Reconnecting " }); this._pollTimer = setTimeout(function(){evs.poll()}, interval||0); }, cleanup: function() { this.log('evs cleaning up') if (this._pollTimer){ clearInterval(this._pollTimer); this._pollTimer = null; } if (this._noActivityTimer){ clearInterval(this._noActivityTimer); this._noActivityTimer = null; } if (this._xhr){ this._xhr.abort(); this._xhr = null; } }, resetNoActivityTimer: function(){ if (this.silentTimeout){ if (this._noActivityTimer){ clearInterval(this._noActivityTimer); } var evs = this; this._noActivityTimer = setTimeout( function(){ evs.log('Timeout! silentTImeout:'+evs.silentTimeout); evs.pollAgain(); }, this.silentTimeout ); } }, close: function () { this.readyState = this.CLOSED; this.log('Closing connection. readyState: '+this.readyState); this.cleanup(); }, ondata: function() { var request = this._xhr; if (request.isReady() && !request.hasError() ) { // reset the timer, as we have activity this.resetNoActivityTimer(); // move this EventSource to OPEN state... if (this.readyState == this.CONNECTING) { this.readyState = this.OPEN; this.dispatchEvent('open', { type: 'open' }); } var buffer = request.getBuffer(); if (buffer.length > this.bufferSizeLimit) { this.log('buffer.length > this.bufferSizeLimit'); this.pollAgain(); } if (this.cursor == 0 && buffer.length > 0){ // skip byte order mark \uFEFF character if it starts the stream if (buffer.substring(0,1) == '\uFEFF'){ this.cursor = 1; } } var lastMessageIndex = this.lastMessageIndex(buffer); if (lastMessageIndex[0] >= this.cursor){ var newcursor = lastMessageIndex[1]; var toparse = buffer.substring(this.cursor, newcursor); this.parseStream(toparse); this.cursor = newcursor; } // if request is finished, reopen the connection if (request.isDone()) { this.log('request.isDone(). reopening the connection'); this.pollAgain(this.interval); } } else if (this.readyState !== this.CLOSED) { this.log('this.readyState !== this.CLOSED'); this.pollAgain(this.interval); //MV: Unsure why an error was previously dispatched } }, parseStream: function(chunk) { // normalize line separators (\r\n,\r,\n) to \n // remove white spaces that may precede \n chunk = this.cache + this.normalizeToLF(chunk); var events = chunk.split('\n\n'); var i, j, eventType, datas, line, retry; for (i=0; i < (events.length - 1); i++) { eventType = 'message'; datas = []; parts = events[i].split('\n'); for (j=0; j < parts.length; j++) { line = this.trimWhiteSpace(parts[j]); if (line.indexOf('event') == 0) { eventType = line.replace(/event:?\s*/, ''); } else if (line.indexOf('retry') == 0) { retry = parseInt(line.replace(/retry:?\s*/, '')); if(!isNaN(retry)) { this.interval = retry; } } else if (line.indexOf('data') == 0) { datas.push(line.replace(/data:?\s*/, '')); } else if (line.indexOf('id:') == 0) { this.lastEventId = line.replace(/id:?\s*/, ''); } else if (line.indexOf('id') == 0) { // this resets the id this.lastEventId = null; } } if (datas.length) { // dispatch a new event var event = new MessageEvent(eventType, datas.join('\n'), window.location.origin, this.lastEventId); this.dispatchEvent(eventType, event); } } this.cache = events[events.length - 1]; }, dispatchEvent: function (type, event) { var handlers = this['_' + type + 'Handlers']; if (handlers) { for (var i = 0; i < handlers.length; i++) { handlers[i].call(this, event); } } if (this['on' + type]) { this['on' + type].call(this, event); } }, addEventListener: function (type, handler) { if (!this['_' + type + 'Handlers']) { this['_' + type + 'Handlers'] = []; } this['_' + type + 'Handlers'].push(handler); }, removeEventListener: function (type, handler) { var handlers = this['_' + type + 'Handlers']; if (!handlers) { return; } for (var i = handlers.length - 1; i >= 0; --i) { if (handlers[i] === handler) { handlers.splice(i, 1); break; } } }, _pollTimer: null, _noactivityTimer: null, _xhr: null, lastEventId: null, cache: '', cursor: 0, onerror: null, onmessage: null, onopen: null, readyState: 0, // =================================================================== // helpers functions // those are attached to prototype to ease reuse and testing... urlWithParams: function (baseURL, params) { var encodedArgs = []; if (params){ var key, urlarg; var urlize = encodeURIComponent; for (key in params){ if (params.hasOwnProperty(key)) { urlarg = urlize(key)+'='+urlize(params[key]); encodedArgs.push(urlarg); } } } if (encodedArgs.length > 0){ if (baseURL.indexOf('?') == -1) return baseURL + '?' + encodedArgs.join('&'); return baseURL + '&' + encodedArgs.join('&'); } return baseURL; }, lastMessageIndex: function(text) { var ln2 =text.lastIndexOf('\n\n'); var lr2 = text.lastIndexOf('\r\r'); var lrln2 = text.lastIndexOf('\r\n\r\n'); if (lrln2 > Math.max(ln2, lr2)) { return [lrln2, lrln2+4]; } return [Math.max(ln2, lr2), Math.max(ln2, lr2) + 2] }, trimWhiteSpace: function(str) { // to remove whitespaces left and right of string var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; return str.replace(reTrim, ''); }, normalizeToLF: function(str) { // replace \r and \r\n with \n return str.replace(/\r\n|\r/g, '\n'); } }; if (!isOldIE()){ EventSource.isPolyfill = "XHR"; // EventSource will send request using XMLHttpRequest EventSource.prototype.XHR = function(evs) { request = new XMLHttpRequest(); this._request = request; evs._xhr = this; // set handlers request.onreadystatechange = function(){ if (request.readyState > 1 && evs.readyState != evs.CLOSED) { if (request.status == 200 || (request.status>=300 && request.status<400)){ evs.ondata(); } else { request._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "The server responded with "+request.status }); evs.close(); } } }; request.onprogress = function () { }; request.open('GET', evs.urlWithParams(evs.URL, evs.getArgs), true); var headers = evs.xhrHeaders; // maybe null for (var header in headers) { if (headers.hasOwnProperty(header)){ request.setRequestHeader(header, headers[header]); } } if (evs.lastEventId) { request.setRequestHeader('Last-Event-Id', evs.lastEventId); } request.send(); }; EventSource.prototype.XHR.prototype = { useXDomainRequest: false, _request: null, _failed: false, // true if we have had errors... isReady: function() { return this._request.readyState >= 2; }, isDone: function() { return (this._request.readyState == 4); }, hasError: function() { return (this._failed || (this._request.status >= 400)); }, getBuffer: function() { var rv = ''; try { rv = this._request.responseText || ''; } catch (e){} return rv; }, abort: function() { if ( this._request ) { this._request.abort(); } } }; } else { EventSource.isPolyfill = "IE_8-9"; // patch EventSource defaultOptions var defaults = EventSource.prototype.defaultOptions; defaults.xhrHeaders = null; // no headers will be sent defaults.getArgs['evs_preamble'] = 2048 + 8; // EventSource will send request using Internet Explorer XDomainRequest EventSource.prototype.XHR = function(evs) { request = new XDomainRequest(); this._request = request; // set handlers request.onprogress = function(){ request._ready = true; evs.ondata(); }; request.onload = function(){ this._loaded = true; evs.ondata(); }; request.onerror = function(){ this._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "XDomainRequest error" }); }; request.ontimeout = function(){ this._failed = true; evs.readyState = evs.CLOSED; evs.dispatchEvent('error', { type: 'error', data: "XDomainRequest timed out" }); }; // XDomainRequest does not allow setting custom headers // If EventSource has enabled the use of GET arguments // we add parameters to URL so that server can adapt the stream... var reqGetArgs = {}; if (evs.getArgs) { // copy evs.getArgs in reqGetArgs var defaultArgs = evs.getArgs; for (var key in defaultArgs) { if (defaultArgs.hasOwnProperty(key)){ reqGetArgs[key] = defaultArgs[key]; } } if (evs.lastEventId){ reqGetArgs['evs_last_event_id'] = evs.lastEventId; } } // send the request request.open('GET', evs.urlWithParams(evs.URL,reqGetArgs)); request.send(); }; EventSource.prototype.XHR.prototype = { useXDomainRequest: true, _request: null, _ready: false, // true when progress events are dispatched _loaded: false, // true when request has been loaded _failed: false, // true if when request is in error isReady: function() { return this._request._ready; }, isDone: function() { return this._request._loaded; }, hasError: function() { return this._request._failed; }, getBuffer: function() { var rv = ''; try { rv = this._request.responseText || ''; } catch (e){} return rv; }, abort: function() { if ( this._request){ this._request.abort(); } } }; } function MessageEvent(type, data, origin, lastEventId) { this.bubbles = false; this.cancelBubble = false; this.cancelable = false; this.data = data || null; this.origin = origin || ''; this.lastEventId = lastEventId || ''; this.type = type || 'message'; } function isOldIE () { //return true if we are in IE8 or IE9 return (window.XDomainRequest && (window.XMLHttpRequest && new XMLHttpRequest().responseType === undefined)) ? true : false; } global[evsImportName] = EventSource; })(this);