@rexxars/eventsource-polyfill
Version:
A browser polyfill for W3C EventSource (http://www.w3.org/TR/eventsource/)
588 lines (479 loc) • 15.5 kB
JavaScript
/*
* EventSource polyfill
* Originally published by sc AmvTek srl (https://github.com/amvtek/EventSource) - devel@amvtek.com
* Forked by Espen Hovlandsdal to fix a few issues + publish latest version
*/
;(function (root, factory) {
/* global define */
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory)
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory()
} else {
// Browser globals (root is window)
if (global.EventSource && !global._eventSourceImportPrefix) {
return
}
var evsImportName = (root._eventSourceImportPrefix || '') + 'EventSource'
root[evsImportName] = factory()
}
})(typeof self === 'undefined' ? this : self, function () {
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
// eslint-disable-next-line no-console
if (typeof console === 'undefined' || typeof console.log === 'undefined') {
this.loggingEnabled = false
}
},
log: function (message) {
if (this.loggingEnabled) {
// eslint-disable-next-line no-console
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 (err) {
// in an attempt to silence the errors
this.log('There were errors inside the pool try-catch')
this.dispatchEvent('error', {type: 'error', data: err.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 = []
var 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*/, ''), 10)
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 && this.readyState != this.CLOSED) {
// dispatch a new event
var event = new MessageEvent(
eventType,
datas.join('\n'),
typeof window !== 'undefined' && typeof window.location !== 'undefined'
? window.location.origin
: null,
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 = '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) {
/* global XDomainRequest */
var 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 (err) {
// intentional noop
}
return rv
},
abort: function () {
if (this._request) {
this._request.abort()
}
},
}
} else {
EventSource.isPolyfill = 'XHR'
// EventSource will send request using XMLHttpRequest
EventSource.prototype.XHR = function (evs) {
var 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 () {
// intentional noop
}
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 (err) {
// intentional noop
}
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 Boolean(
typeof window !== 'undefined' &&
window.XDomainRequest &&
window.XMLHttpRequest &&
new XMLHttpRequest().responseType === undefined
)
}
return EventSource
})