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 <devel@amvtek.com>"
],
"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
================================================
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Polyfill EventSource Test Suite</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.0/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/jasmine.css">
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
<!-- include source files here... -->
<!--<script type="text/javascript" src="src/EventSource.js"></script>-->
<script>
// We define a prefix prior to importing evs
window._eventSourceImportPrefix = 'Test';
</script>
<script type="text/javascript" src="src/eventsource.js"></script>
<!-- include spec files here... -->
<!--<script type="text/javascript" src="spec/EventSourceSpec.js"></script>-->
<script type="text/javascript" src="spec/concurrentSpec.js"></script>
</head>
<body>
<h1> Concurrent polyfill EventSource Test Suite </h1>
<div class="test-infos">
<p>
Be patient, those tests need a few minutes to complete.
</p>
<script>
if (window["EventSource"]) {
document.write('<p>This browser supports EventSource natively</p>')
}
else {
document.write("<p>This browser doesn't support EventSource</p>")
}
</script>
</div>
</body>
</html>
================================================
FILE: javascript/SpecRunner.html
================================================
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Polyfill EventSource Test Suite</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.0/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/jasmine.css">
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
<!-- include source files here... -->
<!--<script type="text/javascript" src="src/EventSource.js"></script>-->
<script>
// We define a prefix prior to importing evs
window._eventSourceImportPrefix = 'Test';
</script>
<script type="text/javascript" src="src/eventsource.js"></script>
<!-- include spec files here... -->
<!--<script type="text/javascript" src="spec/EventSourceSpec.js"></script>-->
<script type="text/javascript" src="spec/eventsourceSpec.js"></script>
</head>
<body>
<h1> Polyfill EventSource Test Suite </h1>
<div class="test-infos">
<p>
Be patient, those tests need a few minutes to complete.
</p>
<script>
if (window["EventSource"]) {
document.write('<p>This browser supports EventSource natively</p>')
}
else {
document.write("<p>This browser doesn't support EventSource</p>")
}
</script>
</div>
</body>
</html>
================================================
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 <devel@amvtek.com>",
"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()
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
SYMBOL INDEX (66 symbols across 8 files)
FILE: dist/eventsource.js
function MessageEvent (line 603) | function MessageEvent(type, data, origin, lastEventId) {
function isOldIE (line 614) | function isOldIE () {
FILE: javascript/spec/concurrentSpec.js
function EVS (line 40) | function EVS(srcUrl){
FILE: javascript/spec/eventsourceSpec.js
function addEventListeners (line 562) | function addEventListeners(evs, done) {
FILE: javascript/src/eventsource.js
function MessageEvent (line 603) | function MessageEvent(type, data, origin, lastEventId) {
function isOldIE (line 614) | function isOldIE () {
FILE: test_server/evsutils/log.py
class LeveledOnlyFileLogObserver (line 24) | class LeveledOnlyFileLogObserver(_FileLogObserver):
method emit (line 30) | def emit(self,eventDict):
function setLeveledLogging (line 37) | def setLeveledLogging():
function buildContextAwareLogPrefix (line 45) | def buildContextAwareLogPrefix(prefix):
class LogPublisher (line 62) | class LogPublisher(object):
method _msg (line 68) | def _msg(self,*args,**kwargs):
method _err (line 73) | def _err(self,_stuff=None,_why=None,**kwargs):
method debug (line 78) | def debug(self,*args,**kwargs):
method msg (line 85) | def msg(self,*args,**kwargs):
method err (line 94) | def err(self,_stuff=None,_why=None,**kwargs):
method getLogger (line 103) | def getLogger(self,logPrefix):
class Logger (line 116) | class Logger(LogPublisher):
method logPrefix (line 119) | def logPrefix(self):
method _msg (line 122) | def _msg(self,*args,**kwargs):
method _err (line 128) | def _err(self,_stuff=None,_why=None,**kwargs):
function setLevel (line 135) | def setLevel(minLevel,defaultLevel):
FILE: test_server/evsutils/protocol.py
class EventSourceRequest (line 19) | class EventSourceRequest(SimpleHTTPRequest):
method parse (line 23) | def parse(self, httpMsg):
class SimpleHTTPServerProtocol (line 76) | class SimpleHTTPServerProtocol(basic.LineReceiver, policies.TimeoutMixin):
method __init__ (line 94) | def __init__(self, maxLength, timeout):
method lineLengthExceeded (line 99) | def lineLengthExceeded(self, line):
method connectionMade (line 104) | def connectionMade(self):
method timeoutConnection (line 111) | def timeoutConnection(self):
method connectionLost (line 116) | def connectionLost(self, reason=None):
method lineReceived (line 122) | def lineReceived(self, line):
method sendError (line 146) | def sendError(self, error):
method startResponse (line 152) | def startResponse(self):
method buildEventStream (line 157) | def buildEventStream(self):
class CooperativePushProducer (line 185) | class CooperativePushProducer(object):
method __init__ (line 189) | def __init__(self, iterator):
method getTaskState (line 193) | def getTaskState(self):
method whenDone (line 197) | def whenDone(self):
method pauseProducing (line 201) | def pauseProducing(self):
method resumeProducing (line 205) | def resumeProducing(self):
method stopProducing (line 209) | def stopProducing(self):
class SimpleHTTPServerProtocolFactory (line 216) | class SimpleHTTPServerProtocolFactory(protocol.Factory):
method __init__ (line 218) | def __init__(self):
method buildProtocol (line 224) | def buildProtocol(self, addr):
FILE: test_server/evsutils/utils.py
function encode_http_response (line 16) | def encode_http_response(
class SimpleHTTPRequest (line 47) | class SimpleHTTPRequest(object):
method __init__ (line 62) | def __init__(self, httpMsg):
method parse (line 76) | def parse(self, httpMsg):
method set_error (line 126) | def set_error(self, status, reason, headers=None, entity=None):
method get_error (line 132) | def get_error(self):
method has_error (line 139) | def has_error(self):
class TestSource (line 145) | class TestSource(object):
method __init__ (line 161) | def __init__(self, seed, length, closeAt=None,
method visit_from (line 185) | def visit_from(self, fromId=0):
class EventSourceEncoder (line 264) | class EventSourceEncoder(object):
method __init__ (line 268) | def __init__(self, linesep='\n', eventsep='\n'):
method encode_preamble (line 272) | def encode_preamble(self, size=2056):
method encode_comment (line 282) | def encode_comment(self, comment):
method encode_mark (line 292) | def encode_mark(self, evtId):
method encode_name (line 302) | def encode_name(self, evtName):
method encode_data (line 311) | def encode_data(self, datas):
method encode_event (line 326) | def encode_event(self, datas, evtName=None, evtId=None):
function split_by (line 347) | def split_by(msg, n):
FILE: test_server/twisted/plugins/test_eventsource.py
class Options (line 13) | class Options(usage.Options):
class TestEventsourceServiceMaker (line 21) | class TestEventsourceServiceMaker(object):
method makeService (line 28) | def makeService(self, options):
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (109K chars).
[
{
"path": ".gitignore",
"chars": 663,
"preview": "# vim swp files\n*.swp\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distributi"
},
{
"path": ".gitmodules",
"chars": 82,
"preview": "[submodule \"docs\"]\n\tpath = docs\n\turl = git@github.com:amvtek/EventSource.wiki.git\n"
},
{
"path": "Gruntfile.js",
"chars": 731,
"preview": "module.exports = function(grunt) {\n\n \"use strict\";\n \n grunt.initConfig({\n\n\tpkg: grunt.file.readJSON('package.js"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 AmvTek\n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "README.md",
"chars": 1969,
"preview": "EventSource Polyfill\n====================\n\nProvide polyfill to support EventSource in browser where it is not available."
},
{
"path": "bower.json",
"chars": 615,
"preview": "{\n \"name\": \"eventsource-polyfill\",\n \"homepage\": \"https://github.com/amvtek/EventSource\",\n \"authors\": [\n \"amvtek <d"
},
{
"path": "dist/browserify-eventsource.js",
"chars": 852,
"preview": "/*\n * CommonJS module that exports EventSource polyfill version 0.9.7\n * This module is intended for browser side us"
},
{
"path": "dist/eventsource.js",
"chars": 17758,
"preview": "/*\n * EventSource polyfill version 0.9.7\n * Supported by sc AmvTek srl\n * :email: devel@amvtek.com\n */\n;(function "
},
{
"path": "javascript/SpecConcurrentRunner.html",
"chars": 1495,
"preview": "<!DOCTYPE HTML>\n<html>\t\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n <title>Polyfill E"
},
{
"path": "javascript/SpecRunner.html",
"chars": 1485,
"preview": "<!DOCTYPE HTML>\n<html>\t\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n <title>Polyfill E"
},
{
"path": "javascript/spec/concurrentSpec.js",
"chars": 4060,
"preview": "\ndescribe('Test concurrent operation of various EventSource against real stream', function(){\n\n var polyfillEventSour"
},
{
"path": "javascript/spec/eventsourceSpec.js",
"chars": 26903,
"preview": "/**\n * Created by vasi on 4/8/14.\n */\n\nvar evsName = \"EventSource\",\n evsImportName = (window._eventSourceImportPrefix"
},
{
"path": "javascript/src/browserify-eventsource.js",
"chars": 953,
"preview": "/*\n * CommonJS module that exports EventSource polyfill version {{VERSION}}\n * This module is intended for browser s"
},
{
"path": "javascript/src/eventsource.js",
"chars": 17764,
"preview": "/*\n * EventSource polyfill version {{VERSION}}\n * Supported by sc AmvTek srl\n * :email: devel@amvtek.com\n */\n;(fun"
},
{
"path": "package.json",
"chars": 957,
"preview": "{\n \"name\": \"eventsource-polyfill\",\n \"version\": \"0.9.7\",\n \"description\": \"A browser polyfill for W3C EventSource (http"
},
{
"path": "test_server/etc/nginx/evs_tests.conf",
"chars": 2089,
"preview": "upstream event_sources {\n\t\n # eventsource web server run from command line \n server 127.0.0.1:7676;\n}\n\nserver{\n\n "
},
{
"path": "test_server/etc/supervisor/evs_test_server.conf",
"chars": 285,
"preview": "[program:test_eventsource]\ncommand=/usr/local/www/testevs.amvtek.com/bin/twistd \n -n --pidfile= \n test_eventsource"
},
{
"path": "test_server/evsutils/__init__.py",
"chars": 46,
"preview": "# from protocol import *\n# from utils import *"
},
{
"path": "test_server/evsutils/log.py",
"chars": 4140,
"preview": "# -*- coding: utf-8 -*-\n\"\"\"\n shared.log\n ~~~~~~~~~~\n\n AmvTek attend to take control of twisted logging\n\n :co"
},
{
"path": "test_server/evsutils/protocol.py",
"chars": 6529,
"preview": "# -*- coding: utf-8 -*-\nimport random as RND\n\ntry:\n from cStringIO import StringIO\nexcept ImportError:\n from Stri"
},
{
"path": "test_server/evsutils/utils.py",
"chars": 11778,
"preview": "# -*- coding: utf-8 -*-\n\nfrom urlparse import urlparse, parse_qsl\nfrom rfc822 import Message as MimeMessage\nimport rand"
},
{
"path": "test_server/requirements.txt",
"chars": 88,
"preview": "Twisted==19.7.0\nargparse==1.2.1\ndistribute==0.6.24\nwsgiref==0.1.2\nzope.interface==4.1.1\n"
},
{
"path": "test_server/twisted/plugins/test_eventsource.py",
"chars": 781,
"preview": "# -*- coding: utf-8 -*-\n\nfrom zope.interface import implements\n\nfrom twisted.application.service import IServiceMaker\nf"
}
]
About this extraction
This page contains the full source code of the amvtek/EventSource GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (100.7 KB), approximately 23.6k tokens, and a symbol index with 66 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.