Repository: amvtek/EventSource Branch: master Commit: 2ced4fad8755 Files: 23 Total size: 100.7 KB Directory structure: gitextract_h5s0yxmq/ ├── .gitignore ├── .gitmodules ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist/ │ ├── browserify-eventsource.js │ └── eventsource.js ├── javascript/ │ ├── SpecConcurrentRunner.html │ ├── SpecRunner.html │ ├── spec/ │ │ ├── concurrentSpec.js │ │ └── eventsourceSpec.js │ └── src/ │ ├── browserify-eventsource.js │ └── eventsource.js ├── package.json └── test_server/ ├── etc/ │ ├── nginx/ │ │ └── evs_tests.conf │ └── supervisor/ │ └── evs_test_server.conf ├── evsutils/ │ ├── __init__.py │ ├── log.py │ ├── protocol.py │ └── utils.py ├── requirements.txt └── twisted/ └── plugins/ └── test_eventsource.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # vim swp files *.swp # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ bin/ build/ develop-eggs/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Twisted plugins cache test_server/twisted/plugins/dropin.cache # local node_modules node_modules/ # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ ================================================ FILE: .gitmodules ================================================ [submodule "docs"] path = docs url = git@github.com:amvtek/EventSource.wiki.git ================================================ FILE: Gruntfile.js ================================================ module.exports = function(grunt) { "use strict"; grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), 'string-replace': { dist: { options: { replacements: [ {pattern: /{{VERSION}}/g, replacement: '<%= pkg.version %>'} ] }, files: { 'dist/eventsource.js': ['javascript/src/eventsource.js'], 'dist/browserify-eventsource.js': ['javascript/src/browserify-eventsource.js'] } } }, uglify: { dist: { files: { 'dist/eventsource.min.js': ['dist/eventsource.js'] } } }, }); grunt.loadNpmTasks('grunt-string-replace'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.registerTask('default', ['string-replace', 'uglify']); }; ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 AmvTek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ EventSource Polyfill ==================== Provide polyfill to support EventSource in browser where it is not available. > - Used in production > - Tested in Internet Explorer 8 + > - Tested in Android browser 2.1 + > - [Documented][] > - Run the [Browser test suite][] Installing ---------- ### from source Download suitable project archive (zip or tar.gz) from [release page][] Include in your html documents one of the following javascript file: > - *dist/eventsource.js* > - *dist/eventsource.min.js* (minified version) ### Using bower package manager To install package from **bower registry**, type : bower install eventsource-polyfill Include in your html documents one of the following javascript file: > - *bower\_components/eventsource-polyfill/dist/eventsource.js* > - *bower\_components/eventsource-polyfill/dist/eventsource.min.js* (minified version) ### Using npm package manager To install package from **npm registry**, type : npm install eventsource-polyfill Note that this package may only be used with in **browser application**. If you are using [browserify][] , you just have to require this package in your main module… ``` sourceCode // load (Polyfill) EventSource, in case browser does not support it... require('eventsource-polyfill'); ``` Run the tests now ----------------- With your web browser visit this [test site][Browser test suite] Allow **sufficient time** ( ~ 5 minutes) for the full Test Suite to run… Project content --------------- dist/ built version of javascript modules javascript/ Contains polyfill module and related unit tests test_server/ python server which generates *easy to test* **event stream** docs/ documentation wiki [Documented]: https://github.com/amvtek/EventSource/wiki [Browser test suite]: http://testevs.amvtek.com/ [release page]: https://github.com/amvtek/EventSource/releases/latest [browserify]: http://browserify.org ================================================ FILE: bower.json ================================================ { "name": "eventsource-polyfill", "homepage": "https://github.com/amvtek/EventSource", "authors": [ "amvtek " ], "description": "A polyfill for http://www.w3.org/TR/eventsource/", "main": ["javascript/src/eventsource.js", "eventsource.min.js", "README.rst"], "keywords": [ "sse", "server sent events", "eventsource", "event-source", "polyfill" ], "license": "MIT", "ignore": [ "**/.*", "javascript", "test_server", "Gruntfile.js", "package.json", "node_modules", "bower_components", "test", "tests", "docs" ] } ================================================ FILE: dist/browserify-eventsource.js ================================================ /* * CommonJS module that exports EventSource polyfill version 0.9.7 * This module is intended for browser side use * ===================================================================== * THIS IS A POLYFILL MODULE, SO IT HAS SIDE EFFECTS * IT AUTOMATICALLY CHECKS IF window OBJECT DEFINES EventSource * AND ADD THE EXPORTED ONE IN CASE IT IS UNDEFINED * ===================================================================== * Supported by sc AmvTek srl * :email: devel@amvtek.com */ var PolyfillEventSource = require('./eventsource.js').EventSource; module.exports = PolyfillEventSource; // Add EventSource to window if it is missing... if (window && !window.EventSource){ window.EventSource = PolyfillEventSource; if (console){ console.log("polyfill-eventsource added missing EventSource to window"); } } ================================================ FILE: dist/eventsource.js ================================================ /* * EventSource polyfill version 0.9.7 * 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(); }, _onxhrdata: 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._onxhrdata(); } 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._onxhrdata(); }; request.onload = function(){ this._loaded = true; evs._onxhrdata(); }; 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); ================================================ FILE: javascript/SpecConcurrentRunner.html ================================================ Polyfill EventSource Test Suite

Concurrent polyfill EventSource Test Suite

Be patient, those tests need a few minutes to complete.

================================================ FILE: javascript/SpecRunner.html ================================================ Polyfill EventSource Test Suite

Polyfill EventSource Test Suite

Be patient, those tests need a few minutes to complete.

================================================ FILE: javascript/spec/concurrentSpec.js ================================================ describe('Test concurrent operation of various EventSource against real stream', function(){ var polyfillEventSource = window['TestEventSource']; var nativeEventSource = window['EventSource']; var TEST_SOURCE_URLS = [ "/test/eventsource/4-messages-with-seed-01", "/test/eventsource/6-messages-with-seed-02", "/test/eventsource/8-messages-closeat-4-with-seed-03", "/test/eventsource/16-messages-closeat-5-with-seed-04", ]; jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000; var WM = function (EventSourceFactory, EventSourceName, sourceUrls){ var self = {}; var _count = sourceUrls.length; self.setdonecb = function(donefunc){ if (_count <= 0 && donefunc ){ donefunc(); } self.donecallback = donefunc; }; self.complete = function(){ --_count; if (_count <= 0 && self.donecallback){ self.donecallback(); } }; function EVS(srcUrl){ var evs = new EventSourceFactory(srcUrl); var ctx = this; ctx.eventsource = evs; ctx.msgEvents = []; evs.addEventListener('message', function (e) { if (e.data) {ctx.msgEvents.push(e.data);} }, false); ctx.openEvents = []; evs.addEventListener('open', function (e) { ctx.openEvents.push(e.type); }, false); ctx.errorEvents = []; evs.addEventListener('error', function (e) { ctx.errorEvents.push(e.type); }, false); ctx.testmetaEvents = []; evs.addEventListener('testmeta', function (e) { ctx.testmetaEvents = JSON.parse(e.data); }, false); ctx.testendEvents = []; evs.addEventListener('testend', function (e) { ctx.testendEvents.push(e.type); self.complete(); evs.close(); }, false); }; // create one EventSource for each url in sourceUrls self.sources = []; for (var i=0; i < sourceUrls.length; i++){ console.log("creating new "+EventSourceName+" on "+sourceUrls[i]); self.sources.push(new EVS(sourceUrls[i])); } return self; }; describe("consume 4 sources concurrently", function(){ describe('using polyfill eventsource', function () { var wm = WM(polyfillEventSource,"polyfill EventSource", TEST_SOURCE_URLS); beforeEach(function (done) { this.sources = wm.sources; wm.setdonecb(done); }); afterEach(function (done){ console.log("polyfill EventSource SPEC OVER..."); done(); }); it("sources.length shall be equal to TEST_SOURCE_URLS.length", function (){ expect(this.sources.length).toEqual(TEST_SOURCE_URLS.length); }); it("data in testmeta event should match all incoming messages data", function () { var result; for (var i=0; i < this.sources.length; i++){ result = this.sources[i]; expect(result.testmetaEvents).toEqual(result.msgEvents); }; }); it("testend event was received", function () { var result; for (var i=0; i < this.sources.length; i++){ result = this.sources[i]; expect(result.testendEvents.length).toEqual(1); }; }); }); if (nativeEventSource){ describe('using native eventsource', function () { var wm = WM(nativeEventSource,"native EventSource", TEST_SOURCE_URLS); beforeEach(function (done) { this.sources = wm.sources; wm.setdonecb(done); }); afterEach(function (done){ console.log("native EventSource SPEC OVER..."); done(); }); it("sources.length shall be equal to TEST_SOURCE_URLS.length", function (){ expect(this.sources.length).toEqual(TEST_SOURCE_URLS.length); }); it("data in testmeta event should match all incoming messages data", function () { var result; for (var i=0; i < this.sources.length; i++){ result = this.sources[i]; expect(result.testmetaEvents).toEqual(result.msgEvents); }; }); it("testend event was received", function () { var result; for (var i=0; i < this.sources.length; i++){ result = this.sources[i]; expect(result.testendEvents.length).toEqual(1); }; }); }); }; }); }); ================================================ FILE: javascript/spec/eventsourceSpec.js ================================================ /** * Created by vasi on 4/8/14. */ var evsName = "EventSource", evsImportName = (window._eventSourceImportPrefix || '') + evsName, isEventSourceSupported = (window["EventSource"] != undefined); describe("Testing EventSource polyfill with MockupXHR", function() { var previousXHR; beforeEach(function() { var mockupXHR = function(evs) { this.responseText = ''; this.evs = evs; }; mockupXHR.prototype = { useXDomaineRequest: false, _request: null, _failed: false, isReady: function() { return true; }, isDone: function() { return false; }, hasError: function() { return (this._failed); }, getBuffer: function() { return this.responseText; }, abort: function() { this.responseText = ''; }, sendData: function(str) { this.responseText += str; this.evs._onxhrdata(); } }; this.eventSource = window[evsImportName]; previousXHR = this.eventSource.prototype.XHR; this.eventSource.prototype.XHR = mockupXHR; }); afterEach(function () { this.eventSource.prototype.XHR = previousXHR; }); describe("Provide MockupXHR class to ease testing the EventSource class", function() { var evs; beforeEach(function (done) { evs = new this.eventSource('http://exampleLoadMockupXHR.com'); setTimeout(function () { done(); }, 0); }); afterEach(function (){ evs.close(); }); it("should load mockupXHR instead of native XHR", function() { // the prefix is set in SpecRunner.html expect(evs._xhr.sendData).toBeDefined(); }); }); describe("Setting up the polyfill EventSource for it to be available alongside browser native EventSource", function() { it("When no options object is passed to the EventSource constructor, all option in EventSource.prototype.defaultOptions are set.", function() { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://example.com'); var expected = evs.defaultOptions; // in this test we expect all options to be the default ones var actual = { loggingEnabled: evs.loggingEnabled, loggingPrefix: evs.loggingPrefix, interval: evs.interval, bufferSizeLimit: evs.bufferSizeLimit, // bytes silentTimeout: evs.silentTimeout, // milliseconds getArgs: evs.getArgs, xhrHeaders: evs.xhrHeaders }; expect(actual).toEqual(expected); evs.close(); }); it("When options is passed to constructor, all option not defined in defaultOptions are ignored.", function() { // the prefix is set in SpecRunner.html var extraOptions = { extraOption: 1000 }; var evs = new this.eventSource('http://example.com', extraOptions); expect(evs.extraOption).toBeUndefined(); evs.close(); }); it("When options is passed to constructor, option defined in defaultOptions are over written.", function() { // the prefix is set in SpecRunner.html var options = { interval: 1000, bufferSizeLimit: 256*1024 // bytes }; var evs = new this.eventSource('http://example.com', options); var defaults = evs.defaultOptions; var expected = { interval: options.interval, bufferSizeLimit: options.bufferSizeLimit, silentTimeout: defaults.silentTimeout, // milliseconds getArgs: defaults.getArgs, xhrHeaders: defaults.xhrHeaders }; var actual = { interval: evs.interval, bufferSizeLimit: evs.bufferSizeLimit, // bytes silentTimeout: evs.silentTimeout, // milliseconds getArgs: evs.getArgs, xhrHeaders: evs.xhrHeaders }; expect(actual).toEqual(expected); evs.close(); }); }); describe("tests for urlWithParams", function() { it("When the baseURL already contains arguments, the rest of evs arguments are appended.", function() { var params = { a: 1, b: 2 }; var evs = new this.eventSource('http://example.com?ciao=ola'); var urlWithParams = evs.urlWithParams; expect(urlWithParams('http://example.com?ciao=ola', params)).toBe('http://example.com?ciao=ola&a=1&b=2'); }) it("When called with a null or undefined params, baseUrl is returned.", function() { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://exampleurlWithParams.com'); var urlWithParams = evs.urlWithParams; expect(urlWithParams('http://example.com')).toBe('http://example.com'); }); it("When called with a params object that contains no attribute but has a prototype with attributes, baseUrl is returned.", function() { // the prefix is set in SpecRunner.html var Params = function () {}; Params.prototype = { 'a': 1, 'b': 2 }; var evs = new this.eventSource('http://exampleurlWithParams.com'); var urlWithParams = evs.urlWithParams; expect(urlWithParams('http://example.com', new Params())).toBe('http://example.com'); }); it("When called with a params that define arguments, those are added to the url", function() { // the prefix is set in SpecRunner.html var params = { a: 1, b: 2 }; var evs = new this.eventSource('http://exampleurlWithParams.com'); var urlWithParams = evs.urlWithParams; expect(urlWithParams('http://example.com', params)).toBe('http://example.com?a=1&b=2'); }); }); describe("tests for lastLineIndex", function() { it("returns correct lastMessageIndex in case of \\n\\n", function() { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://exampleurlWithParams.com'); var lastLineIndex = evs.lastMessageIndex; expect(lastLineIndex("0123\r\r678\n\n9")).toEqual([9, 11]); }); it("returns correct lastLineIndex in case of \\r\\r", function() { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://exampleurlWithParams.com'); var lastLineIndex = evs.lastMessageIndex; expect(lastLineIndex("0123\n\n678\r\r")).toEqual([9, 11]); }); it("returns correct lastLineIndex in case of \\r\\n\\r\\n", function() { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://exampleurlWithParams.com'); var lastLineIndex = evs.lastMessageIndex; expect(lastLineIndex("0123\n\n67\r\n\r\n")).toEqual([8, 12]); }); }); describe("tests for trimWhiteSpace", function() { it("should remove whitespace left and right of the string", function () { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://exampleurlWithParams.com'); var trimWhiteSpace = evs.trimWhiteSpace; expect(trimWhiteSpace(" text between spaces ")).toBe('text between spaces'); }); }); describe("tests for normalizeToLF", function() { it("should replace CR and CRLF with LF(\\n) inside a string", function () { // the prefix is set in SpecRunner.html var evs = new this.eventSource('http://example.com'); var normalizeToLF = evs.normalizeToLF; var str = "LF:\n. CR:\r. CRLF:\r\n. and again LF:\n. CR:\r. CRLF:\r\n. Double CR:\r\r. Double CRLF:\r\n\r\n"; var expectedStr = "LF:\n. CR:\n. CRLF:\n. and again LF:\n. CR:\n. CRLF:\n. Double CR:\n\n. Double CRLF:\n\n"; var actualStr = normalizeToLF(str); expect(actualStr).toBe(expectedStr); evs.close(); }); }); describe("Simulates EventSource stream using MockupXHR", function() { var evs, receivedMessageEvents, receivedOpenEvents, receivedErrorEvents; beforeEach(function (done) { evs = new this.eventSource('http://exampleSimulatesEventSourceWithMockupXHR.com'); receivedMessageEvents = []; receivedOpenEvents = []; receivedErrorEvents = []; evs.addEventListener('message', function(e) { receivedMessageEvents.push(e.data); }, false); evs.addEventListener('open', function(e) { receivedOpenEvents.push(e.type); }, false); evs.addEventListener('error', function(e) { receivedErrorEvents.push(e.type); }, false); setTimeout(function () { done(); }, 0) }); afterEach(function (done) { evs.close(); done(); }); it("check that Mockup XHR is used", function () { expect(evs._xhr.sendData).toBeDefined(); }); it("Initial BOM mark is skipped", function () { evs._xhr.sendData('\ufeffdata: "First line of data."\r\r'); var expectedEvents = ['"First line of data."']; expect(receivedMessageEvents).toEqual(expectedEvents); }); it("Multiline message are properly reassembled", function () { evs._xhr.sendData('data: "First line of data."\ndata: "Second line of data."\r\r'); var expectedEvents = ['"First line of data."\n"Second line of data."']; expect(receivedMessageEvents).toEqual(expectedEvents); }); it("Chunky transmissions are properly buffered", function () { evs._xhr.sendData('data: "First line of data."\ndata: "Second line of data."\n\ndata: "First part of broken message.'); evs._xhr.sendData('Second part of broken message."\n\n'); var expectedEvents = ['"First line of data."\n"Second line of data."', '"First part of broken message.Second part of broken message."']; expect(receivedMessageEvents).toEqual(expectedEvents); }); it("id lines are properly processed", function () { var expectedEventId = '1983'; evs._xhr.sendData('data: "First line of data."\ndata: "Second line of data."\n\ndata: "First part of broken message.'); evs._xhr.sendData('Second part of broken message."\n\nid: '+expectedEventId+'\ndata: "Message with new id"\n\n'); expect(evs.lastEventId).toEqual(expectedEventId); }); it("option bufferSizeLimit is taken into account", function () { evs.bufferSizeLimit = 100; evs._xhr.sendData('data: "First line of data."\ndata: "Second line of data."\n\ndata: "First part of broken message.'); evs._xhr.sendData('Second part of broken message."\n\nid: 1983\rdata: "Message with new id"\n\n'); evs._xhr.sendData('data: "Message to force dispatching the open event"\n\n'); var expectedOpenEvents = ["open", "open"]; expect(receivedOpenEvents).toEqual(expectedOpenEvents); }); describe("Manually ticking the Jasmine Clock to produce a sleep until events are fired:", function () { var previous = 0; beforeEach(function() { previous = evs.silentTimeout; jasmine.clock().install(); }); afterEach(function(done) { evs.silentTimeout = previous; jasmine.clock().uninstall(); done(); }); it("option silentTimeout is taken into account", function () { evs.silentTimeout = 1000; evs._xhr.sendData('data: "First line of data."\ndata: "Second line of data."\n\r\ndata: "First part of broken message.'); evs._xhr.sendData('Second part of broken message."\n\r\nid: 1983\rdata: "Message with new id"\n\n'); setTimeout(function() { }, 1100); jasmine.clock().tick(1101); evs._xhr.sendData('data: "Message to force dispatching the open event"\n\n'); var expectedOpenEvents = ["open", "open"]; var expectedErrorEvents = ["error"]; expect(receivedOpenEvents).toEqual(expectedOpenEvents); expect(receivedErrorEvents).toEqual(expectedErrorEvents); }); }); }); }); describe("Evaluating EventSource 'time to attach listener' doubt", function() { var evs, previousXHR, receivedMessageEvents = [], receivedOpenEvents = [], receivedErrorEvents = []; beforeEach(function (done) { var mockupXHR = function (evs) { this.responseText = ''; this.evs = evs; evs._xhr = this; // send data right away to stress the events this.sendData(this.initialData || ''); }; mockupXHR.prototype = { initialData: 'data: "I will fire very quick"\n\n', useXDomaineRequest: false, _request: null, _failed: false, isReady: function () { return true; }, isDone: function () { return false; }, hasError: function () { return (this._failed); }, getBuffer: function () { return this.responseText; }, abort: function () { }, sendData: function (str) { evs = this.evs; this.responseText += str; evs._onxhrdata(); } }; this.eventSource = window[evsImportName]; previousXHR = this.eventSource.prototype.XHR; this.eventSource.prototype.XHR = mockupXHR; evs = new this.eventSource('http://exampleTimeToAttachDoubt.com'); evs.addEventListener('message', function(e) { receivedMessageEvents.push(e.data); }, false); evs.addEventListener('open', function(e) { receivedOpenEvents.push(e.type); }, false); evs.addEventListener('error', function(e) { receivedErrorEvents.push(e.data); }, false); setTimeout(function () { done(); }, 0); }); afterEach(function (done) { evs.close(); this.eventSource.prototype.XHR = previousXHR; done(); }); it("should catch initial message sent", function (done) { var expectedMessageEvents = ['"I will fire very quick"']; expect(receivedMessageEvents).toEqual(expectedMessageEvents); done(); }) }); describe('Failed XHR request(invalid url) shall trigger EventSource to close and "error" event to be dispatched', function() { var evs, receivedMessageEvents = [], receivedOpenEvents = [], receivedErrorEvents = []; beforeEach(function (done) { this.eventSource = window[evsImportName]; // sending to wrong url evs = new this.eventSource('http://exampleFailedXHRequest'); evs.addEventListener('message', function (e) { receivedMessageEvents.push(e.data); }, false); evs.addEventListener('open', function (e) { receivedOpenEvents.push(e.type); }, false); evs.addEventListener('error', function (e) { receivedErrorEvents.push(e.type); done(); }, false); }); afterEach(function (done) { evs.close(); receivedErrorEvents = []; done(); }); it("should send error event", function (done) { expect(receivedErrorEvents.length).toBe(1); done(); }); }); describe('Failed XHR request(missing-code 404) shall trigger EventSource to close and "error" event to be dispatched', function() { var evs, receivedMessageEvents = [], receivedOpenEvents = [], receivedErrorEvents = []; beforeEach(function (done) { this.eventSource = window[evsImportName]; // sending to wrong url evs = new this.eventSource('/missing'); evs.addEventListener('message', function (e) { receivedMessageEvents.push(e.data); }, false); evs.addEventListener('open', function (e) { receivedOpenEvents.push(e.type); }, false); evs.addEventListener('error', function (e) { receivedErrorEvents.push(e.type); done(); }, false); }); afterEach(function () { evs.close() }); it("should send error event", function (done) { expect(receivedErrorEvents.length).toBe(1); done(); }); }); describe('Tests with twisted server:', function() { var evs, receivedMessageEvents = [], receivedOpenEvents = [], receivedErrorEvents = [], receivedTestMetaEvents = [], receivedTestEndEvents = []; function addEventListeners(evs, done) { evs.addEventListener('message', function (e) { // if (e.lastEventId) {console.log(e.lastEventId);} // console.log('message: ' + e.type + ":" + e.data) // console.log(e) if (e.data) {receivedMessageEvents.push(e.data);} }, false); evs.addEventListener('open', function (e) { receivedOpenEvents.push(e.type); }, false); evs.addEventListener('error', function (e) { receivedErrorEvents.push(e.type); }, false); evs.addEventListener('testmeta', function (e) { // console.log("received testmeta:" + e.type + "["+ evs.id +"]: "+ e.data); // console.log(e) receivedTestMetaEvents = JSON.parse(e.data); }, false); evs.addEventListener('testend', function (e) { receivedTestEndEvents.push(e.type); // console.log('received tesend' + e.type + ":" + e.data + "end"); // console.log(e) done(); }, false); } afterEach(function (done) { evs.close(); receivedErrorEvents = []; receivedMessageEvents = []; receivedTestEndEvents = []; receivedTestMetaEvents = []; receivedOpenEvents = []; done(); }); describe('4-messages-with-seed-01', function() { var twistedUrl = '/test/eventsource/4-messages-with-seed-01'; describe('using polyfill eventsource', function () { beforeEach(function (done) { var eventSource = window[evsImportName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toEqual(1); }) }); }); if (isEventSourceSupported) { describe('using native eventsource', function () { beforeEach(function (done) { var eventSource = window[evsName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toEqual(1); }) }); }) } }); describe('6-messages-with-seed-02', function() { var twistedUrl = '/test/eventsource/6-messages-with-seed-02'; describe('using polyfill eventsource', function () { beforeEach(function (done) { var eventSource = window[evsImportName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toEqual(1); }) }); }); if (isEventSourceSupported) { describe('using native eventsource', function () { beforeEach(function (done) { var eventSource = window[evsName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toEqual(1); }) }); }) } }); describe('8-messages-closeat-4-with-seed-03', function() { var twistedUrl = '/test/eventsource/8-messages-closeat-4-with-seed-03'; describe('using polyfill eventsource', function () { beforeEach(function (done) { var eventSource = window[evsImportName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toBe(1); }); it("at least 1 error message are received, meaning 1 reconnection", function () { expect(receivedErrorEvents.length).toBeGreaterThan(0); // we expect at least 1 }); }); }); if (isEventSourceSupported) { describe('using native eventsource', function () { beforeEach(function (done) { var eventSource = window[evsName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toBe(1); }); it("at least 1 error messages are received, meaning 1 reconnection", function () { expect(receivedErrorEvents.length).toBeGreaterThan(0); }); }); }) } }); describe('16-messages-closeat-5-with-seed-04', function() { var twistedUrl = '/test/eventsource/16-messages-closeat-5-with-seed-04'; describe('using polyfill eventsource', function () { beforeEach(function (done) { var eventSource = window[evsImportName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toBe(1); }); it("3 error messages are received, meaning 3 reconnections", function () { expect(receivedErrorEvents.length).toBeGreaterThan(1); // we expect at least 3 }); }); }); if (isEventSourceSupported) { describe('using native eventsource', function () { beforeEach(function (done) { var eventSource = window[evsName]; evs = new eventSource(twistedUrl); addEventListeners(evs, done); }); describe ('after a complete run until testend:', function() { it("data in testmeta event should match all incoming messages data", function () { expect(receivedTestMetaEvents).toEqual(receivedMessageEvents); }); it("testend event is received", function () { expect(receivedTestEndEvents.length).toBe(1); }); it("3 error messages are received, meaning 3 reconnections", function () { expect(receivedErrorEvents.length).toBeGreaterThan(1); }); }); }) } }); }); ================================================ FILE: javascript/src/browserify-eventsource.js ================================================ /* * CommonJS module that exports EventSource polyfill version {{VERSION}} * This module is intended for browser side use * ===================================================================== * THIS IS A POLYFILL MODULE, SO IT HAS SIDE EFFECTS * IT AUTOMATICALLY CHECKS IF window OBJECT DEFINES EventSource * AND ADD THE EXPORTED ONE IN CASE IT IS UNDEFINED * ===================================================================== * Supported by sc AmvTek srl * :email: devel@amvtek.com */ var PolyfillEventSource = require('./eventsource.js').EventSource; module.exports = PolyfillEventSource; // Add EventSource to window if it is missing... if (window && !window.EventSource){ window.EventSource = PolyfillEventSource; // Don't break IE < 10 if (typeof console !== "undefined" && typeof console.log !== "undefined"){ console.log("polyfill-eventsource added missing EventSource to window"); } } ================================================ FILE: javascript/src/eventsource.js ================================================ /* * EventSource polyfill version {{VERSION}} * 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(); }, _onxhrdata: 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._onxhrdata(); } 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._onxhrdata(); }; request.onload = function(){ this._loaded = true; evs._onxhrdata(); }; 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); ================================================ FILE: package.json ================================================ { "name": "eventsource-polyfill", "version": "0.9.7", "description": "A browser polyfill for W3C EventSource (http://www.w3.org/TR/eventsource/)", "main": "dist/browserify-eventsource.js", "directories": { "doc": "docs" }, "files": [ "dist/eventsource.js", "dist/browserify-eventsource.js" ], "scripts": { "test": "echo \"Error: to run browser tests, visit http://testevs.amvtek.com/\" && exit 1" }, "repository": { "type": "git", "url": "https://github.com/amvtek/EventSource.git" }, "keywords": [ "sse", "server sent events", "eventsource", "event-source", "polyfill" ], "author": "amvtek ", "license": "MIT", "bugs": { "url": "https://github.com/amvtek/EventSource/issues" }, "homepage": "https://github.com/amvtek/EventSource", "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-uglify": "^0.6.0", "grunt-string-replace": "^1.0.0" } } ================================================ FILE: test_server/etc/nginx/evs_tests.conf ================================================ upstream event_sources { # eventsource web server run from command line server 127.0.0.1:7676; } server{ listen 80; # this is for HTTP server_name testevs.amvtek.com; root /usr/local/www/testevs.amvtek.com/EventSource/javascript; index SpecRunner.html; location = /favicon.ico { return 404; } location = /robots.txt { alias /usr/local/www/robots/deny_all.txt; } location = /test/eventsource/4-messages-with-seed-01{ chunked_transfer_encoding off; proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Browser-Addr $remote_addr; proxy_set_header X-EVS-Test-Num-Message 4; proxy_pass http://event_sources; access_log /var/log/nginx/test_event_source.log time_upstream_fmt; } location = /test/eventsource/6-messages-with-seed-02{ chunked_transfer_encoding off; proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Browser-Addr $remote_addr; proxy_set_header X-EVS-Test-Num-Message 6; proxy_pass http://event_sources; access_log /var/log/nginx/test_event_source.log time_upstream_fmt; } location = /test/eventsource/8-messages-closeat-4-with-seed-03{ chunked_transfer_encoding off; proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Browser-Addr $remote_addr; proxy_set_header X-EVS-Test-Num-Message 8; proxy_set_header X-EVS-Test-CloseAt 4; proxy_pass http://event_sources; access_log /var/log/nginx/test_event_source.log time_upstream_fmt; } location = /test/eventsource/16-messages-closeat-5-with-seed-04{ chunked_transfer_encoding off; proxy_http_version 1.1; proxy_buffering off; proxy_set_header Host $host; proxy_set_header X-Browser-Addr $remote_addr; proxy_set_header X-EVS-Test-Num-Message 16; proxy_set_header X-EVS-Test-CloseAt 5; proxy_pass http://event_sources; access_log /var/log/nginx/test_event_source.log time_upstream_fmt; } } ================================================ FILE: test_server/etc/supervisor/evs_test_server.conf ================================================ [program:test_eventsource] command=/usr/local/www/testevs.amvtek.com/bin/twistd -n --pidfile= test_eventsource --host=127.0.0.1 --port=7676 user=eventsource directory=/usr/local/www/testevs.amvtek.com/EventSource/test_server process_name=%(program_name)s num_procs=1 ================================================ FILE: test_server/evsutils/__init__.py ================================================ # from protocol import * # from utils import * ================================================ FILE: test_server/evsutils/log.py ================================================ # -*- coding: utf-8 -*- """ shared.log ~~~~~~~~~~ AmvTek attend to take control of twisted logging :copyright: (c) 2012 by sc AmvTek srl :email: devel@amvtek.com """ import sys from logging import DEBUG,INFO,WARNING,ERROR,FATAL from zope.interface import implements from twisted.python import context, log as _log ILogContext = _log.ILogContext _FileLogObserver = _log.FileLogObserver # local alias class LeveledOnlyFileLogObserver(_FileLogObserver): """ FileLogObserver that filters out non 'leveled' log events... This is our attend to eliminate unwanted twisted log messages... """ def emit(self,eventDict): """skip logging if eventDict does not contain 'level' key...""" if eventDict.get('isError') or eventDict.has_key('level'): return _FileLogObserver.emit(self, eventDict) def setLeveledLogging(): """ monkey patch twisted.python.log.FileLogObserver so that 'unleveled' log events are ignored... """ _log.FileLogObserver = LeveledOnlyFileLogObserver def buildContextAwareLogPrefix(prefix): """ return logPrefix callable that : appends context retrieved 'system' to set prefix """ def logPrefix(): "logPrefix that adjust to current context" logCtx = context.get('system',"-") if logCtx is not "-": return "%s,%s"%(logCtx,prefix) return prefix return logPrefix class LogPublisher(object): minLevel = DEBUG defaultLevel = INFO def _msg(self,*args,**kwargs): "wraps twisted.log.msg..." _log.msg(*args,**kwargs) def _err(self,_stuff=None,_why=None,**kwargs): "wraps twisted.log.err..." _log.err(_stuff,_why,**kwargs) def debug(self,*args,**kwargs): "bypass twisted log.msg in case DEBUG below minLevel..." if DEBUG >= self.minLevel: kwargs['level'] = DEBUG self._msg(*args,**kwargs) def msg(self,*args,**kwargs): "bypass twisted log.msg in case level below minLevel..." level = kwargs.setdefault('level',self.defaultLevel) if level >= self.minLevel: self._msg(*args,**kwargs) log = msg def err(self,_stuff=None,_why=None,**kwargs): """ bypass twisted log.err in case level below minLevel uses ERROR as default for level """ level = kwargs.setdefault('level',ERROR) if level >= self.minLevel: self._err(_stuff,_why,**kwargs) def getLogger(self,logPrefix): "return Logger instance" logger = Logger() logger.minLevel = self.minLevel logger.defaultLevel = self.defaultLevel if callable(logPrefix): logger.logPrefix = logPrefix else: logger.logPrefix = lambda :logPrefix return logger class Logger(LogPublisher): "a Logger which inline 'level aware' debug, msg, err log methods" def logPrefix(self): return "?" def _msg(self,*args,**kwargs): "add 'system' into log 'event dict'..." kwargs['system'] = self.logPrefix() _log.msg(*args,**kwargs) def _err(self,_stuff=None,_why=None,**kwargs): "add 'system' into log 'event dict'..." kwargs['system'] = self.logPrefix() _log.err(_stuff,_why,**kwargs) def setLevel(minLevel,defaultLevel): "set minimum and default levels for log publishing" minLevel = int(minLevel) defaultLevel = int(defaultLevel) # Initializes global publisher thePublisher.minLevel = minLevel thePublisher.defaultLevel = defaultLevel # Initializes Logger class, this simplify inlining Logger Logger.minLevel = minLevel Logger.defaultLevel = defaultLevel # Install global LogPublisher thePublisher = LogPublisher() debug = thePublisher.debug msg = thePublisher.msg err = thePublisher.err getLogger = thePublisher.getLogger # Add globals to ease replacing twisted.python.log with this module callWithContext = _log.callWithContext callWithLogger = _log.callWithLogger startConsoleLogging = lambda :_log.startLogging(sys.stdout) ================================================ FILE: test_server/evsutils/protocol.py ================================================ # -*- coding: utf-8 -*- import random as RND try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from zope.interface import implements from twisted.protocols import basic, policies from twisted.internet import reactor, protocol, interfaces from twisted.internet.task import deferLater, cooperate import log as customLog from utils import SimpleHTTPRequest, encode_http_response, TestSource, split_by class EventSourceRequest(SimpleHTTPRequest): ALLOWED_METHODS = frozenset(["GET"]) def parse(self, httpMsg): super(EventSourceRequest, self).parse(httpMsg) if self._error is not None: return self.evsArgs = {} # continue parsing to read test parameters try: errReason = "Bad Request" # local aliases path = self.path reqArgs = self.args reqHeaders = self.headers # read seed errReason = "Invalid seed" self.evsArgs['seed'] = hash(path.rsplit('/',1)[-1]) # read message sequence length errReason = "Can not parse sequence length" seqlength = reqArgs.get('evs_num_messages', None) or \ reqHeaders.get('X-EVS-Test-Num-Message'.lower(), None) or None self.evsArgs['length'] = int(seqlength) # read closeAt errReason = "Can not parse closeAt" closeAt = reqArgs.get('evs_close_at', None) or\ reqHeaders.get('X-EVS-Test-CloseAt'.lower(), None) self.evsArgs['closeAt'] = int(closeAt) if closeAt else None # read sendPreamble self.evsArgs['sendPreamble'] = bool(reqArgs.get('evs_preamble')) # read Last-Event-Id errReason = "Invalid Last-Event-Id" lastEventId = reqArgs.get('evs_last_event_id', None) or\ reqHeaders.get('Last-Event-Id'.lower(), None) or -1 self.evsLastId = int(lastEventId) except ValueError: self.set_error(400, errReason) except: self.set_error(500, "Server side error") class SimpleHTTPServerProtocol(basic.LineReceiver, policies.TimeoutMixin): START_EVENT_STREAM = \ "HTTP/1.1 200 OK\r\n"\ "Content-Type: text/event-stream\r\n"\ "Access-Control-Allow-Origin: *\r\n"\ "Cache-Control: no-cache\r\n"\ "Transfert-Encoding: identity\r\n"\ "Connection: close\r\n\r\n" MAX_LENGTH_ERROR = encode_http_response(413, 'Request Entity Too Large') MAX_REQUEST_TRANSMIT_TIME_ERROR = encode_http_response(408, 'Request Timeout') delimiter = "\r\n\r\n" request = None def __init__(self, maxLength, timeout): self.MAX_LENGTH = maxLength self.MAX_REQUEST_TRANSMIT_TIME = timeout def lineLengthExceeded(self, line): self.log.msg("request too large, disconnecting") self.sendError(self.MAX_LENGTH_ERROR) def connectionMade(self): self.setTimeout(self.MAX_REQUEST_TRANSMIT_TIME) self.log = customLog.getLogger(self.transport.logstr) self.log.msg('connectionMade') def timeoutConnection(self): self.log.msg("request transmission takes too long, timing out") self.sendError(self.MAX_REQUEST_TRANSMIT_TIME_ERROR) def connectionLost(self, reason=None): self.log.msg('Connection closed because %s' % reason) if hasattr(self, 'producer'): self.producer.stopProducing() def lineReceived(self, line): """parse incoming http request, and start streaming...""" self.log.msg("received HTTP request, attending to parse it") self.resetTimeout() self.request = EventSourceRequest(line) if self.request._error is not None: self.log.msg("Got HTTP error %(status)s" % self.request._error) self.sendError(self.request.error) else: # send response headers self.startResponse() # register EventSource producer self.producer = CooperativePushProducer(self.buildEventStream()) self.transport.registerProducer(self.producer, True) d = self.producer.whenDone() d.addCallback(lambda _: self.transport.loseConnection()) def sendError(self, error): """send error response and close connection""" self.transport.write(error) self.transport.loseConnection() def startResponse(self): """send response that starts EventSource stream...""" self.transport.write(self.START_EVENT_STREAM) def buildEventStream(self): lastEvtId = self.request.evsLastId evtSource = TestSource(messages=self.factory.messages, **self.request.evsArgs) evtSequence = evtSource.visit_from(lastEvtId+1) if lastEvtId > -1: self.log.msg("restart streaming from : %d" % lastEvtId) else: self.log.msg("new eventsource stream...") restart = lambda: None for message in evtSequence: # extract start of message... msgstart = message[:message.find(":", 0, 8)+12] self.log.msg("new event line : %s..." % msgstart) for part in split_by(message, RND.randint(1, 3)): self.transport.write(part) yield deferLater(reactor, RND.uniform(0.05, 0.3), restart) class CooperativePushProducer(object): implements(interfaces.IPushProducer) def __init__(self, iterator): self.task = cooperate(iterator) def getTaskState(self): return self.task._completionState def whenDone(self): return self.task.whenDone() def pauseProducing(self): self.task.pause() def resumeProducing(self): self.task.resume() def stopProducing(self): if self.task._completionState is None: self.task.stop() class SimpleHTTPServerProtocolFactory(protocol.Factory): def __init__(self): self.MAX_LENGTH = 100000 self.MAX_REQUEST_TRANSMIT_TIME = 20000 # seconds self.messages = None def buildProtocol(self, addr): proto = SimpleHTTPServerProtocol(self.MAX_LENGTH, self.MAX_REQUEST_TRANSMIT_TIME) proto.factory = self return proto if __name__ == "__main__": import doctest doctest.testmod() ================================================ FILE: test_server/evsutils/utils.py ================================================ # -*- coding: utf-8 -*- from urlparse import urlparse, parse_qsl from rfc822 import Message as MimeMessage import random as RND import json try: from cStringIO import StringIO except ImportError: from StringIO import StringIO import log as customLog def encode_http_response( status, reason, version="HTTP/1.1", headers=None, entity=None, **kwargs): """return http message encoding response""" buf = [] # 'dictify' headers headers = dict(headers or []) # add status line buf.append("%s %i %s\r\n" % (version, status, reason)) # add entity description in headers if entity: headers["Content-Length"] = len(entity) headers.setdefault("Content-Type", "text/plain") # render headers for name, value in headers.items(): buf.append("%s: %s\r\n" % (name.title(), value)) # add empty line buf.append("\r\n") if entity: buf.append(entity) return "".join(buf) class SimpleHTTPRequest(object): """Simple HTTPRequest object to help parsing HTTP message""" ALLOWED_METHODS = frozenset([ "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"]) method = None path = None headers = {} version = None args = None _error = None def __init__(self, httpMsg): """ >>> reqText = "GET /path/to/my/eventsource?arg1=1&arg2=2 HTTP/1.1\\r\\nheader: 3\\r\\n\\r\\n" >>> req = SimpleHTTPRequest(reqText) >>> req.path, req.args, req.method, req.version, req.headers ('/path/to/my/eventsource', {'arg1': '1', 'arg2': '2'}, 'GET', (1, 1), {'header': '3'}) """ self.log = customLog.getLogger('Processing HTTP request') try: self.parse(httpMsg) except: self.set_error(400, "Bad Request") def parse(self, httpMsg): """parse and validate http request out of httpMsg""" f = StringIO(httpMsg) # parse request line reqline = f.readline() parts = reqline.split() if len(parts) == 3: method, path, version = parts elif len(parts) == 2: method, path = parts version = "HTTP/0.9" else: return self.set_error(400, "Invalid Request line") # validates method method = method.strip().upper() if method not in self.ALLOWED_METHODS: hdrs = {"Allow": ", ".join(self.ALLOWED_METHODS)} return self.set_error(405, "Method Not Allowed", hdrs) self.method = method # validates path self.path = urlparse(path).path self.args = dict(parse_qsl(urlparse(path).query)) # validates version version = version.strip().upper() if not version.startswith("HTTP/"): return self.set_error(400, "Invalid HTTP version") majmin = version[5:].split(".") try: major, minor = [int(v) for v in majmin] except: return self.set_error(400, "Invalid HTTP version") self.version = (major, minor) # parse headers self.headers = dict(MimeMessage(f)) self.log.msg('\nFound headers: {}'.format(self.headers)) def set_error(self, status, reason, headers=None, entity=None): """helper method allowing to define 'shortcut' response""" status = int(status) self._error = locals() def get_error(self): """return string encoding error response if any""" if self._error: return encode_http_response(**self._error) error = property(get_error) def has_error(self): """return True if request is not valid""" return bool(self._error) class TestSource(object): lineSep = ['\n', '\r', '\r\n'] messages = [u'one line. one', u'two lines. one\ntwo lines. two', u'three lines. one\nthree lines. two\nthree lines. three', u'four lines. one\nfour. two\nfour lines. three\nfour lines. four', u'spam, ham and eggs', u"spam, șuncă și ouă", u'spam\nham\neggs', u'Nobody expects the spanish inquisition', u"Personne ne s'attend à l'Inquisition espagnole", u'always look on the bright side of bugs', u"toujours regarder le côté lumineux de bugs"] def __init__(self, seed, length, closeAt=None, sendPreamble=False, messages=None): """ test if we can recreate the exact scenario with the same seed >>> source1 = TestSource(2014, 2) >>> source2 = TestSource(2014, 2) >>> source1.sequence == source2.sequence True """ RND.seed(seed) self.sendPreamble = sendPreamble self.length = int(length) self.closeAt = closeAt self.messages = [unicode(mess) for mess in messages or self.messages] self.chosenLnSep = RND.choice(self.lineSep) # self.chosenEvtSep = RND.choice(self.lineSep).rjust(RND.randint(2, 10)) self.chosenEvtSep = self.chosenLnSep # RND.choice(self.lineSep) self.sequence = ["Message %02i%s" % (n, RND.choice(self.messages)) for n in xrange(self.length)] self.encoder = EventSourceEncoder(self.chosenLnSep, self.chosenEvtSep) def visit_from(self, fromId=0): """ generator function, let us iterate sequence from identifier helper lambda to simulate event encoding >>> ev = lambda mylist, sep: ["%s%s" % (el, sep) for el in mylist] start from fromId=0 to closeAt=1 >>> source = TestSource(2014, 10, 1) # closeAt is 1 >>> source.sequence = [1, 2, 3] >>> actual = list(source.visit_from()) >>> sep = source.chosenLnSep + source.chosenEvtSep >>> expected = ev(['event: testmeta\\ndata: [1, 2, 3]', 'data: 1\\nid: 1'], sep) >>> actual == expected True start from fromId=7 to closeAt=4 >>> source = TestSource(2014, 10, 4) # closeAt is 4 >>> source.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> actual = list(source.visit_from(7)) >>> sep = source.chosenLnSep + source.chosenEvtSep >>> expected = ev(['data: 7\\nid: 7', 'data: 8\\nid: 8'], sep) >>> actual == expected True start from fromId=7 to closeAt=10 >>> source = TestSource(2014, 10) # to the end >>> source.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> actual = list(source.visit_from(7)) >>> sep = source.chosenLnSep + source.chosenEvtSep >>> expected = ev(['data: 7\\nid: 7', 'data: 8\\nid: 8', 'data: 9\\nid: 9', 'data: 10\\nid: 10', 'event: testend\\ndata: This is the end'], sep) >>> actual == expected True start from fromId=5 to closeAt=4 >>> source = TestSource(2014, 8, 4) # to the end >>> source.sequence = [1, 2, 3, 4, 5, 6, 7, 8] >>> actual = list(source.visit_from(5)) >>> sep = source.chosenLnSep + source.chosenEvtSep >>> expected = ev(['data: 5\\nid: 5', 'data: 6\\nid: 6', 'data: 7\\nid: 7', 'data: 8\\nid: 8', 'event: testend\\ndata: This is the end'], sep) >>> actual == expected True tart from fromId=5 to closeAt=4 >>> source = TestSource(2014, 16, 4) # to the end >>> source.sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] >>> actual = list(source.visit_from(5)) >>> sep = source.chosenLnSep + source.chosenEvtSep >>> expected = ev(['data: 5\\nid: 5', 'data: 6\\nid: 6', 'data: 7\\nid: 7', 'data: 8\\nid: 8'], sep) >>> actual == expected True """ fromId = int(fromId) encoder = self.encoder sequence = self.sequence closeAt = self.closeAt encode = encoder.encode_event sendPreamble = self.sendPreamble events = [encode(message, None, index+1) for index, message in enumerate(sequence)] events.insert(0, encode(json.dumps(sequence), 'testmeta', 0)) events.append(encode("This is the end", 'testend')) if sendPreamble: yield encoder.encode_preamble() seqEnd = fromId - fromId % closeAt + closeAt + 1 if closeAt else len(events)+1 seqEnd = seqEnd if seqEnd < len(events)-1 else len(events) for event in events[fromId:seqEnd]: yield event class EventSourceEncoder(object): preamble = "mypreamble" def __init__(self, linesep='\n', eventsep='\n'): self.linesep = linesep self.eventsep = eventsep def encode_preamble(self, size=2056): """ return preamble comment aiming at resetting IE 8 9 XDomainRequest >>> EventSourceEncoder().encode_preamble(15) ':mypreamble \\n' """ return (u"%s%s" % ((":%s" % self.preamble).ljust(size), self.linesep)).encode('utf-8') def encode_comment(self, comment): """ return comment line >>> EventSourceEncoder().encode_comment('spam, ham and eggs') u': spam, ham and eggs\\n' """ return u": %s%s" % (comment, self.linesep) def encode_mark(self, evtId): """ return line encoding event id >>> EventSourceEncoder().encode_mark(2014) u'id: 2014\\n' """ return u"id: %s%s" % (evtId, self.linesep) def encode_name(self, evtName): """ return line encoding event name >>> EventSourceEncoder().encode_name('myevent') u'event: myevent\\n' """ return u'event: %s%s' % (evtName, self.linesep) def encode_data(self, datas): ur""" return lines encoding event data >>> datas = 'first data\nsecond data\nthird data' >>> EventSourceEncoder().encode_data(datas) u'data: first data\ndata: second data\ndata: third data\n' >>> datas = u"toujours regarder le côté lumineux de bugs" >>> EventSourceEncoder().encode_data(datas) u'data: toujours regarder le c\xf4t\xe9 lumineux de bugs\n' """ return "data: %s%s" % (("%sdata: " % self.linesep).join(unicode(datas).splitlines()), self.linesep) def encode_event(self, datas, evtName=None, evtId=None): """ return block encoding event >>> datas = 'first data\\nsecond data' >>> EventSourceEncoder().encode_event(datas, 'myevent', 2014) 'event: myevent\\ndata: first data\\ndata: second data\\nid: 2014\\n\\n' >>> datas = 'only one line of data' >>> EventSourceEncoder().encode_event(datas) 'data: only one line of data\\n\\n' """ return (u"%s%s%s%s" % ( self.encode_name(evtName) if evtName else '', self.encode_data(datas), self.encode_mark(evtId) if evtId else '', self.eventsep )).encode('utf-8') def split_by(msg, n): """ return msg divided in nchunk if msg size allow so... >>> split_by('123456789', 3) ['123', '456', '789'] >>> split_by('123456789', 2) ['1234', '56789'] >>> split_by('12345', 2) ['12', '345'] >>> len(split_by('123456789', 4)) 4 Generate all randomly(msg, msg length and pieces) and check if the final length matches >>> import random,string >>> pieces = random.randint(1, 20) >>> len(split_by(''.join(random.choice(string.ascii_uppercase) for i in range(random.randint(1, 100))), pieces)) == pieces True """ return [(msg[len(msg)*i//n:len(msg)*(i+1)//n]) for i in range(n)] if __name__ == "__main__": import doctest doctest.testmod() ================================================ FILE: test_server/requirements.txt ================================================ Twisted==19.7.0 argparse==1.2.1 distribute==0.6.24 wsgiref==0.1.2 zope.interface==4.1.1 ================================================ FILE: test_server/twisted/plugins/test_eventsource.py ================================================ # -*- coding: utf-8 -*- from zope.interface import implements from twisted.application.service import IServiceMaker from twisted.application import internet from twisted.plugin import IPlugin from twisted.python import usage from evsutils.protocol import SimpleHTTPServerProtocolFactory class Options(usage.Options): optParameters = [ ['host', 'h', '0.0.0.0', "host"], ['port', 'p', 7676, "port"] ] class TestEventsourceServiceMaker(object): implements(IServiceMaker, IPlugin) tapname = "test_eventsource" description = "test eventsource" options = Options def makeService(self, options): return internet.TCPServer(int(options["port"]), SimpleHTTPServerProtocolFactory()) serviceMaker = TestEventsourceServiceMaker()