Created
May 24, 2017 02:15
-
-
Save mgrandy/a897720f5ab91d7e09d9315ad10666bc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @license | |
* eventsource.js | |
* Available under MIT License (MIT) | |
* https://github.com/Yaffle/EventSource/ | |
*/ | |
/*jslint indent: 2, vars: true, plusplus: true */ | |
/*global setTimeout, clearTimeout */ | |
(function (global) { | |
"use strict"; | |
var setTimeout = global.setTimeout; | |
var clearTimeout = global.clearTimeout; | |
var k = function () { | |
}; | |
function XHRTransport(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg) { | |
this._internal = new XHRTransportInternal(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg); | |
} | |
XHRTransport.prototype.open = function (url, withCredentials) { | |
this._internal.open(url, withCredentials); | |
}; | |
XHRTransport.prototype.cancel = function () { | |
this._internal.cancel(); | |
}; | |
function XHRTransportInternal(xhr, onStartCallback, onProgressCallback, onFinishCallback, thisArg) { | |
this.onStartCallback = onStartCallback; | |
this.onProgressCallback = onProgressCallback; | |
this.onFinishCallback = onFinishCallback; | |
this.thisArg = thisArg; | |
this.xhr = xhr; | |
this.state = 0; | |
this.charOffset = 0; | |
this.offset = 0; | |
this.url = ""; | |
this.withCredentials = false; | |
this.timeout = 0; | |
} | |
XHRTransportInternal.prototype.onStart = function () { | |
if (this.state === 1) { | |
this.state = 2; | |
var status = 0; | |
var statusText = ""; | |
var contentType = undefined; | |
if (!("contentType" in this.xhr)) { | |
try { | |
status = this.xhr.status; | |
statusText = this.xhr.statusText; | |
contentType = this.xhr.getResponseHeader("Content-Type"); | |
} catch (error) { | |
// https://bugs.webkit.org/show_bug.cgi?id=29121 | |
status = 0; | |
statusText = ""; | |
contentType = undefined; | |
// FF < 14, WebKit | |
// https://bugs.webkit.org/show_bug.cgi?id=29658 | |
// https://bugs.webkit.org/show_bug.cgi?id=77854 | |
} | |
} else { | |
status = 200; | |
statusText = "OK"; | |
contentType = this.xhr.contentType; | |
} | |
if (contentType == undefined) { | |
contentType = ""; | |
} | |
this.onStartCallback.call(this.thisArg, status, statusText, contentType); | |
} | |
}; | |
XHRTransportInternal.prototype.onProgress = function () { | |
this.onStart(); | |
if (this.state === 2 || this.state === 3) { | |
this.state = 3; | |
var responseText = ""; | |
try { | |
responseText = this.xhr.responseText; | |
} catch (error) { | |
// IE 8 - 9 with XMLHttpRequest | |
} | |
var chunkStart = this.charOffset; | |
var length = responseText.length; | |
for (var i = this.offset; i < length; i += 1) { | |
var c = responseText.charCodeAt(i); | |
if (c === "\n".charCodeAt(0) || c === "\r".charCodeAt(0)) { | |
this.charOffset = i + 1; | |
} | |
} | |
this.offset = length; | |
var chunk = responseText.slice(chunkStart, this.charOffset); | |
this.onProgressCallback.call(this.thisArg, chunk); | |
} | |
}; | |
XHRTransportInternal.prototype.onFinish = function () { | |
// IE 8 fires "onload" without "onprogress | |
this.onProgress(); | |
if (this.state === 3) { | |
this.state = 4; | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
this.timeout = 0; | |
} | |
this.onFinishCallback.call(this.thisArg); | |
} | |
}; | |
XHRTransportInternal.prototype.onReadyStateChange = function () { | |
if (this.xhr != undefined) { // Opera 12 | |
if (this.xhr.readyState === 4) { | |
if (this.xhr.status === 0) { | |
this.onFinish(); | |
} else { | |
this.onFinish(); | |
} | |
} else if (this.xhr.readyState === 3) { | |
this.onProgress(); | |
} else if (this.xhr.readyState === 2) { | |
// Opera 10.63 throws exception for `this.xhr.status` | |
// this.onStart(); | |
} | |
} | |
}; | |
XHRTransportInternal.prototype.onTimeout2 = function () { | |
this.timeout = 0; | |
var tmp = (/^data\:([^,]*?)(base64)?,([\S]*)$/).exec(this.url); | |
var contentType = tmp[1]; | |
var data = tmp[2] === "base64" ? global.atob(tmp[3]) : decodeURIComponent(tmp[3]); | |
if (this.state === 1) { | |
this.state = 2; | |
this.onStartCallback.call(this.thisArg, 200, "OK", contentType); | |
} | |
if (this.state === 2 || this.state === 3) { | |
this.state = 3; | |
this.onProgressCallback.call(this.thisArg, data); | |
} | |
if (this.state === 3) { | |
this.state = 4; | |
this.onFinishCallback.call(this.thisArg); | |
} | |
}; | |
XHRTransportInternal.prototype.onTimeout1 = function () { | |
this.timeout = 0; | |
this.open(this.url, this.withCredentials); | |
}; | |
XHRTransportInternal.prototype.onTimeout0 = function () { | |
var that = this; | |
this.timeout = setTimeout(function () { | |
that.onTimeout0(); | |
}, 500); | |
if (this.xhr.readyState === 3) { | |
this.onProgress(); | |
} | |
}; | |
XHRTransportInternal.prototype.handleEvent = function (event) { | |
if (event.type === "load") { | |
this.onFinish(); | |
} else if (event.type === "error") { | |
this.onFinish(); | |
} else if (event.type === "abort") { | |
// improper fix to match Firefox behaviour, but it is better than just ignore abort | |
// see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=880200 | |
// https://code.google.com/p/chromium/issues/detail?id=153570 | |
// IE 8 fires "onload" without "onprogress | |
this.onFinish(); | |
} else if (event.type === "progress") { | |
this.onProgress(); | |
} else if (event.type === "readystatechange") { | |
this.onReadyStateChange(); | |
} | |
}; | |
XHRTransportInternal.prototype.open = function (url, withCredentials) { | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
this.timeout = 0; | |
} | |
this.url = url; | |
this.withCredentials = withCredentials; | |
this.state = 1; | |
this.charOffset = 0; | |
this.offset = 0; | |
var that = this; | |
var tmp = (/^data\:([^,]*?)(?:;base64)?,[\S]*$/).exec(url); | |
if (tmp != undefined) { | |
this.timeout = setTimeout(function () { | |
that.onTimeout2(); | |
}, 0); | |
return; | |
} | |
// loading indicator in Safari, Chrome < 14 | |
// loading indicator in Firefox | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=736723 | |
if ((!("ontimeout" in this.xhr) || ("sendAsBinary" in this.xhr) || ("mozAnon" in this.xhr)) && global.document != undefined && global.document.readyState != undefined && global.document.readyState !== "complete") { | |
this.timeout = setTimeout(function () { | |
that.onTimeout1(); | |
}, 4); | |
return; | |
} | |
// XDomainRequest#abort removes onprogress, onerror, onload | |
this.xhr.onload = function (event) { | |
that.handleEvent({type: "load"}); | |
}; | |
this.xhr.onerror = function () { | |
that.handleEvent({type: "error"}); | |
}; | |
this.xhr.onabort = function () { | |
that.handleEvent({type: "abort"}); | |
}; | |
this.xhr.onprogress = function () { | |
that.handleEvent({type: "progress"}); | |
}; | |
// IE 8-9 (XMLHTTPRequest) | |
// Firefox 3.5 - 3.6 - ? < 9.0 | |
// onprogress is not fired sometimes or delayed | |
// see also #64 | |
this.xhr.onreadystatechange = function () { | |
that.handleEvent({type: "readystatechange"}); | |
}; | |
this.xhr.open("GET", url, true); | |
// withCredentials should be set after "open" for Safari and Chrome (< 19 ?) | |
this.xhr.withCredentials = withCredentials; | |
this.xhr.responseType = "text"; | |
if ("setRequestHeader" in this.xhr) { | |
// Request header field Cache-Control is not allowed by Access-Control-Allow-Headers. | |
// "Cache-control: no-cache" are not honored in Chrome and Firefox | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=428916 | |
//this.xhr.setRequestHeader("Cache-Control", "no-cache"); | |
this.xhr.setRequestHeader("Accept", "text/event-stream"); | |
// Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. | |
//this.xhr.setRequestHeader("Last-Event-ID", this.lastEventId); | |
} | |
try { | |
this.xhr.send(undefined); | |
} catch (error1) { | |
// Safari 5.1.7, Opera 12 | |
throw error1; | |
} | |
if (("readyState" in this.xhr) && global.opera != undefined) { | |
// workaround for Opera issue with "progress" events | |
this.timeout = setTimeout(function () { | |
that.onTimeout0(); | |
}, 0); | |
} | |
}; | |
XHRTransportInternal.prototype.cancel = function () { | |
if (this.state !== 0 && this.state !== 4) { | |
this.state = 4; | |
this.xhr.onload = k; | |
this.xhr.onerror = k; | |
this.xhr.onabort = k; | |
this.xhr.onprogress = k; | |
this.xhr.onreadystatechange = k; | |
this.xhr.abort(); | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
this.timeout = 0; | |
} | |
this.onFinishCallback.call(this.thisArg); | |
} | |
this.state = 0; | |
}; | |
function Map() { | |
this._data = {}; | |
} | |
Map.prototype.get = function (key) { | |
return this._data[key + "~"]; | |
}; | |
Map.prototype.set = function (key, value) { | |
this._data[key + "~"] = value; | |
}; | |
Map.prototype["delete"] = function (key) { | |
delete this._data[key + "~"]; | |
}; | |
function EventTarget() { | |
this._listeners = new Map(); | |
} | |
function throwError(e) { | |
setTimeout(function () { | |
throw e; | |
}, 0); | |
} | |
EventTarget.prototype.dispatchEvent = function (event) { | |
event.target = this; | |
var type = event.type.toString(); | |
var listeners = this._listeners; | |
var typeListeners = listeners.get(type); | |
if (typeListeners == undefined) { | |
return; | |
} | |
var length = typeListeners.length; | |
var listener = undefined; | |
for (var i = 0; i < length; i += 1) { | |
listener = typeListeners[i]; | |
try { | |
if (typeof listener.handleEvent === "function") { | |
listener.handleEvent(event); | |
} else { | |
listener.call(this, event); | |
} | |
} catch (e) { | |
throwError(e); | |
} | |
} | |
}; | |
EventTarget.prototype.addEventListener = function (type, callback) { | |
type = type.toString(); | |
var listeners = this._listeners; | |
var typeListeners = listeners.get(type); | |
if (typeListeners == undefined) { | |
typeListeners = []; | |
listeners.set(type, typeListeners); | |
} | |
for (var i = typeListeners.length; i >= 0; i -= 1) { | |
if (typeListeners[i] === callback) { | |
return; | |
} | |
} | |
typeListeners.push(callback); | |
}; | |
EventTarget.prototype.removeEventListener = function (type, callback) { | |
type = type.toString(); | |
var listeners = this._listeners; | |
var typeListeners = listeners.get(type); | |
if (typeListeners == undefined) { | |
return; | |
} | |
var length = typeListeners.length; | |
var filtered = []; | |
for (var i = 0; i < length; i += 1) { | |
if (typeListeners[i] !== callback) { | |
filtered.push(typeListeners[i]); | |
} | |
} | |
if (filtered.length === 0) { | |
listeners["delete"](type); | |
} else { | |
listeners.set(type, filtered); | |
} | |
}; | |
function Event(type) { | |
this.type = type; | |
this.target = undefined; | |
} | |
function MessageEvent(type, options) { | |
Event.call(this, type); | |
this.data = options.data; | |
this.lastEventId = options.lastEventId; | |
} | |
MessageEvent.prototype = Event.prototype; | |
var XHR = global.XMLHttpRequest; | |
var XDR = global.XDomainRequest; | |
var isCORSSupported = XHR != undefined && (new XHR()).withCredentials != undefined; | |
var Transport = isCORSSupported || (XHR != undefined && XDR == undefined) ? XHR : XDR; | |
var WAITING = -1; | |
var CONNECTING = 0; | |
var OPEN = 1; | |
var CLOSED = 2; | |
var AFTER_CR = 3; | |
var FIELD_START = 4; | |
var FIELD = 5; | |
var VALUE_START = 6; | |
var VALUE = 7; | |
var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i; | |
var MINIMUM_DURATION = 1000; | |
var MAXIMUM_DURATION = 18000000; | |
var getDuration = function (value, def) { | |
var n = value; | |
if (n !== n) { | |
n = def; | |
} | |
return (n < MINIMUM_DURATION ? MINIMUM_DURATION : (n > MAXIMUM_DURATION ? MAXIMUM_DURATION : n)); | |
}; | |
var fire = function (that, f, event) { | |
try { | |
if (typeof f === "function") { | |
f.call(that, event); | |
} | |
} catch (e) { | |
throwError(e); | |
} | |
}; | |
function EventSource(url, options) { | |
EventTarget.call(this); | |
this.onopen = undefined; | |
this.onmessage = undefined; | |
this.onerror = undefined; | |
this.url = ""; | |
this.readyState = CONNECTING; | |
this.withCredentials = false; | |
this._internal = new EventSourceInternal(this, url, options); | |
} | |
function EventSourceInternal(es, url, options) { | |
this.url = url.toString(); | |
this.readyState = CONNECTING; | |
this.withCredentials = isCORSSupported && options != undefined && Boolean(options.withCredentials); | |
this.es = es; | |
this.initialRetry = getDuration(1000, 0); | |
this.heartbeatTimeout = getDuration(45000, 0); | |
this.lastEventId = ""; | |
this.retry = this.initialRetry; | |
this.wasActivity = false; | |
var CurrentTransport = options != undefined && options.Transport != undefined ? options.Transport : Transport; | |
var xhr = new CurrentTransport(); | |
this.transport = new XHRTransport(xhr, this.onStart, this.onProgress, this.onFinish, this); | |
this.timeout = 0; | |
this.currentState = WAITING; | |
this.dataBuffer = []; | |
this.lastEventIdBuffer = ""; | |
this.eventTypeBuffer = ""; | |
this.state = FIELD_START; | |
this.fieldStart = 0; | |
this.valueStart = 0; | |
this.es.url = this.url; | |
this.es.readyState = this.readyState; | |
this.es.withCredentials = this.withCredentials; | |
this.onTimeout(); | |
} | |
EventSourceInternal.prototype.onStart = function (status, statusText, contentType) { | |
if (this.currentState === CONNECTING) { | |
if (contentType == undefined) { | |
contentType = ""; | |
} | |
if (status === 200 && contentTypeRegExp.test(contentType)) { | |
this.currentState = OPEN; | |
this.wasActivity = true; | |
this.retry = this.initialRetry; | |
this.readyState = OPEN; | |
this.es.readyState = OPEN; | |
var event = new Event("open"); | |
this.es.dispatchEvent(event); | |
fire(this.es, this.es.onopen, event); | |
} else if (status !== 0) { | |
var message = ""; | |
if (status !== 200) { | |
message = "EventSource's response has a status " + status + " " + statusText.replace(/\s+/g, " ") + " that is not 200. Aborting the connection."; | |
} else { | |
message = "EventSource's response has a Content-Type specifying an unsupported type: " + contentType.replace(/\s+/g, " ") + ". Aborting the connection."; | |
} | |
throwError(new Error(message)); | |
this.close(); | |
var event = new Event("error"); | |
this.es.dispatchEvent(event); | |
fire(this.es, this.es.onerror, event); | |
} | |
} | |
}; | |
EventSourceInternal.prototype.onProgress = function (chunk) { | |
if (this.currentState === OPEN) { | |
var length = chunk.length; | |
if (length !== 0) { | |
this.wasActivity = true; | |
} | |
for (var position = 0; position < length; position += 1) { | |
var c = chunk.charCodeAt(position); | |
if (this.state === AFTER_CR && c === "\n".charCodeAt(0)) { | |
this.state = FIELD_START; | |
} else { | |
if (this.state === AFTER_CR) { | |
this.state = FIELD_START; | |
} | |
if (c === "\r".charCodeAt(0) || c === "\n".charCodeAt(0)) { | |
if (this.state !== FIELD_START) { | |
if (this.state === FIELD) { | |
this.valueStart = position + 1; | |
} | |
var field = chunk.slice(this.fieldStart, this.valueStart - 1); | |
var value = chunk.slice(this.valueStart + (this.valueStart < position && chunk.charCodeAt(this.valueStart) === " ".charCodeAt(0) ? 1 : 0), position); | |
if (field === "data") { | |
this.dataBuffer.push(value); | |
} else if (field === "id") { | |
this.lastEventIdBuffer = value; | |
} else if (field === "event") { | |
this.eventTypeBuffer = value; | |
} else if (field === "retry") { | |
this.initialRetry = getDuration(Number(value), this.initialRetry); | |
this.retry = this.initialRetry; | |
} else if (field === "heartbeatTimeout") { | |
this.heartbeatTimeout = getDuration(Number(value), this.heartbeatTimeout); | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
var that = this; | |
this.timeout = setTimeout(function () { | |
that.onTimeout(); | |
}, this.heartbeatTimeout); | |
} | |
} | |
} | |
if (this.state === FIELD_START) { | |
if (this.dataBuffer.length !== 0) { | |
this.lastEventId = this.lastEventIdBuffer; | |
if (this.eventTypeBuffer === "") { | |
this.eventTypeBuffer = "message"; | |
} | |
var event = new MessageEvent(this.eventTypeBuffer, { | |
data: this.dataBuffer.join("\n"), | |
lastEventId: this.lastEventIdBuffer | |
}); | |
this.es.dispatchEvent(event); | |
if (this.eventTypeBuffer === "message") { | |
fire(this.es, this.es.onmessage, event); | |
} | |
if (this.currentState === CLOSED) { | |
return; | |
} | |
} | |
this.dataBuffer.length = 0; | |
this.eventTypeBuffer = ""; | |
} | |
this.state = c === "\r".charCodeAt(0) ? AFTER_CR : FIELD_START; | |
} else { | |
if (this.state === FIELD_START) { | |
this.fieldStart = position; | |
this.state = FIELD; | |
} | |
if (this.state === FIELD) { | |
if (c === ":".charCodeAt(0)) { | |
this.valueStart = position + 1; | |
this.state = VALUE_START; | |
} | |
} else if (this.state === VALUE_START) { | |
this.state = VALUE; | |
} | |
} | |
} | |
} | |
} | |
}; | |
EventSourceInternal.prototype.onFinish = function () { | |
if (this.currentState === OPEN || this.currentState === CONNECTING) { | |
this.currentState = WAITING; | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
this.timeout = 0; | |
} | |
if (this.retry > this.initialRetry * 16) { | |
this.retry = this.initialRetry * 16; | |
} | |
if (this.retry > MAXIMUM_DURATION) { | |
this.retry = MAXIMUM_DURATION; | |
} | |
var that = this; | |
this.timeout = setTimeout(function () { | |
that.onTimeout(); | |
}, this.retry); | |
this.retry = this.retry * 2 + 1; | |
this.readyState = CONNECTING; | |
this.es.readyState = CONNECTING; | |
var event = new Event("error"); | |
this.es.dispatchEvent(event); | |
fire(this.es, this.es.onerror, event); | |
} | |
}; | |
EventSourceInternal.prototype.onTimeout = function () { | |
this.timeout = 0; | |
if (this.currentState !== WAITING) { | |
if (!this.wasActivity) { | |
throwError(new Error("No activity within " + this.heartbeatTimeout + " milliseconds. Reconnecting.")); | |
this.transport.cancel(); | |
} else { | |
this.wasActivity = false; | |
var that = this; | |
this.timeout = setTimeout(function () { | |
that.onTimeout(); | |
}, this.heartbeatTimeout); | |
} | |
return; | |
} | |
this.wasActivity = false; | |
var that = this; | |
this.timeout = setTimeout(function () { | |
that.onTimeout(); | |
}, this.heartbeatTimeout); | |
this.currentState = CONNECTING; | |
this.dataBuffer.length = 0; | |
this.eventTypeBuffer = ""; | |
this.lastEventIdBuffer = this.lastEventId; | |
this.fieldStart = 0; | |
this.valueStart = 0; | |
this.state = FIELD_START; | |
var s = this.url.slice(0, 5); | |
if (s !== "data:" && s !== "blob:") { | |
s = this.url + ((this.url.indexOf("?", 0) === -1 ? "?" : "&") + "lastEventId=" + encodeURIComponent(this.lastEventId) + "&r=" + (Math.random() + 1).toString().slice(2)); | |
} else { | |
s = this.url; | |
} | |
try { | |
this.transport.open(s, this.withCredentials); | |
} catch (error) { | |
this.close(); | |
throw error; | |
} | |
}; | |
EventSourceInternal.prototype.close = function () { | |
this.currentState = CLOSED; | |
this.transport.cancel(); | |
if (this.timeout !== 0) { | |
clearTimeout(this.timeout); | |
this.timeout = 0; | |
} | |
this.readyState = CLOSED; | |
this.es.readyState = CLOSED; | |
}; | |
function F() { | |
this.CONNECTING = CONNECTING; | |
this.OPEN = OPEN; | |
this.CLOSED = CLOSED; | |
} | |
F.prototype = EventTarget.prototype; | |
EventSource.prototype = new F(); | |
EventSource.prototype.close = function () { | |
this._internal.close(); | |
}; | |
F.call(EventSource); | |
if (isCORSSupported) { | |
EventSource.prototype.withCredentials = undefined; | |
} | |
var isEventSourceSupported = function () { | |
// Opera 12 fails this test, but this is fine. | |
return global.EventSource != undefined && ("withCredentials" in global.EventSource.prototype); | |
}; | |
if (Transport != undefined && (global.EventSource == undefined || (isCORSSupported && !isEventSourceSupported()))) { | |
// Why replace a native EventSource ? | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=444328 | |
// https://bugzilla.mozilla.org/show_bug.cgi?id=831392 | |
// https://code.google.com/p/chromium/issues/detail?id=260144 | |
// https://code.google.com/p/chromium/issues/detail?id=225654 | |
// ... | |
global.NativeEventSource = global.EventSource; | |
global.EventSource = EventSource; | |
} | |
}(typeof window !== 'undefined' ? window : this)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment