rsnext/packages/next/client/event-source-polyfill.js
2019-04-09 21:49:44 -04:00

850 lines
24 KiB
JavaScript

/* eslint-disable */
// Improved version of https://github.com/Yaffle/EventSource/
// Available under MIT License (MIT)
// Only tries to support IE11 and nothing below
import fetch from 'unfetch'
var document = window.document
var Response = window.Response
var TextDecoder = window.TextDecoder
var TextEncoder = window.TextEncoder
var AbortController = window.AbortController
if (AbortController == undefined) {
AbortController = function () {
this.signal = null
this.abort = function () {
}
}
}
function TextDecoderPolyfill () {
this.bitsNeeded = 0
this.codePoint = 0
}
TextDecoderPolyfill.prototype.decode = function (octets) {
function valid (codePoint, shift, octetsCount) {
if (octetsCount === 1) {
return codePoint >= 0x0080 >> shift && codePoint << shift <= 0x07FF
}
if (octetsCount === 2) {
return codePoint >= 0x0800 >> shift && codePoint << shift <= 0xD7FF || codePoint >= 0xE000 >> shift && codePoint << shift <= 0xFFFF
}
if (octetsCount === 3) {
return codePoint >= 0x010000 >> shift && codePoint << shift <= 0x10FFFF
}
throw new Error()
}
function octetsCount (bitsNeeded, codePoint) {
if (bitsNeeded === 6 * 1) {
return codePoint >> 6 > 15 ? 3 : codePoint > 31 ? 2 : 1
}
if (bitsNeeded === 6 * 2) {
return codePoint > 15 ? 3 : 2
}
if (bitsNeeded === 6 * 3) {
return 3
}
throw new Error()
}
var REPLACER = 0xFFFD
var string = ''
var bitsNeeded = this.bitsNeeded
var codePoint = this.codePoint
for (var i = 0; i < octets.length; i += 1) {
var octet = octets[i]
if (bitsNeeded !== 0) {
if (octet < 128 || octet > 191 || !valid(codePoint << 6 | octet & 63, bitsNeeded - 6, octetsCount(bitsNeeded, codePoint))) {
bitsNeeded = 0
codePoint = REPLACER
string += String.fromCharCode(codePoint)
}
}
if (bitsNeeded === 0) {
if (octet >= 0 && octet <= 127) {
bitsNeeded = 0
codePoint = octet
} else if (octet >= 192 && octet <= 223) {
bitsNeeded = 6 * 1
codePoint = octet & 31
} else if (octet >= 224 && octet <= 239) {
bitsNeeded = 6 * 2
codePoint = octet & 15
} else if (octet >= 240 && octet <= 247) {
bitsNeeded = 6 * 3
codePoint = octet & 7
} else {
bitsNeeded = 0
codePoint = REPLACER
}
if (bitsNeeded !== 0 && !valid(codePoint, bitsNeeded, octetsCount(bitsNeeded, codePoint))) {
bitsNeeded = 0
codePoint = REPLACER
}
} else {
bitsNeeded -= 6
codePoint = codePoint << 6 | octet & 63
}
if (bitsNeeded === 0) {
if (codePoint <= 0xFFFF) {
string += String.fromCharCode(codePoint)
} else {
string += String.fromCharCode(0xD800 + (codePoint - 0xFFFF - 1 >> 10))
string += String.fromCharCode(0xDC00 + (codePoint - 0xFFFF - 1 & 0x3FF))
}
}
}
this.bitsNeeded = bitsNeeded
this.codePoint = codePoint
return string
}
// Firefox < 38 throws an error with stream option
var supportsStreamOption = function () {
try {
return new TextDecoder().decode(new TextEncoder().encode('test'), { stream: true }) === 'test'
} catch (error) {
console.log(error)
}
return false
}
// IE, Edge
if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) {
TextDecoder = TextDecoderPolyfill
}
var k = function () {
}
function XHRWrapper (xhr) {
this.withCredentials = false
this.responseType = ''
this.readyState = 0
this.status = 0
this.statusText = ''
this.responseText = ''
this.onprogress = k
this.onreadystatechange = k
this._contentType = ''
this._xhr = xhr
this._sendTimeout = 0
this._abort = k
}
XHRWrapper.prototype.open = function (method, url) {
this._abort(true)
var that = this
var xhr = this._xhr
var state = 1
var timeout = 0
this._abort = function (silent) {
if (that._sendTimeout !== 0) {
clearTimeout(that._sendTimeout)
that._sendTimeout = 0
}
if (state === 1 || state === 2 || state === 3) {
state = 4
xhr.onload = k
xhr.onerror = k
xhr.onabort = k
xhr.onprogress = k
xhr.onreadystatechange = k
// IE 8 - 9: XDomainRequest#abort() does not fire any event
// Opera < 10: XMLHttpRequest#abort() does not fire any event
xhr.abort()
if (timeout !== 0) {
clearTimeout(timeout)
timeout = 0
}
if (!silent) {
that.readyState = 4
that.onreadystatechange()
}
}
state = 0
}
var onStart = function () {
if (state === 1) {
// state = 2;
var status = 0
var statusText = ''
var contentType = undefined
if (!('contentType' in xhr)) {
try {
status = xhr.status
statusText = xhr.statusText
contentType = xhr.getResponseHeader('Content-Type')
} catch (error) {
// IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3
// Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2
// https://bugs.webkit.org/show_bug.cgi?id=29121
status = 0
statusText = ''
contentType = undefined
// Firefox < 14, Chrome ?, Safari ?
// https://bugs.webkit.org/show_bug.cgi?id=29658
// https://bugs.webkit.org/show_bug.cgi?id=77854
}
} else {
status = 200
statusText = 'OK'
contentType = xhr.contentType
}
if (status !== 0) {
state = 2
that.readyState = 2
that.status = status
that.statusText = statusText
that._contentType = contentType
that.onreadystatechange()
}
}
}
var onProgress = function () {
onStart()
if (state === 2 || state === 3) {
state = 3
var responseText = ''
try {
responseText = xhr.responseText
} catch (error) {
// IE 8 - 9 with XMLHttpRequest
}
that.readyState = 3
that.responseText = responseText
that.onprogress()
}
}
var onFinish = function () {
// Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3)
// IE 8 fires "onload" without "onprogress"
onProgress()
if (state === 1 || state === 2 || state === 3) {
state = 4
if (timeout !== 0) {
clearTimeout(timeout)
timeout = 0
}
that.readyState = 4
that.onreadystatechange()
}
}
var onReadyStateChange = function () {
if (xhr != undefined) { // Opera 12
if (xhr.readyState === 4) {
onFinish()
} else if (xhr.readyState === 3) {
onProgress()
} else if (xhr.readyState === 2) {
onStart()
}
}
}
var onTimeout = function () {
timeout = setTimeout(function () {
onTimeout()
}, 500)
if (xhr.readyState === 3) {
onProgress()
}
}
// XDomainRequest#abort removes onprogress, onerror, onload
xhr.onload = onFinish
xhr.onerror = onFinish
// 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
xhr.onabort = onFinish
// https://bugzilla.mozilla.org/show_bug.cgi?id=736723
if (!('sendAsBinary' in XMLHttpRequest.prototype) && !('mozAnon' in XMLHttpRequest.prototype)) {
xhr.onprogress = onProgress
}
// IE 8 - 9 (XMLHTTPRequest)
// Opera < 12
// Firefox < 3.5
// Firefox 3.5 - 3.6 - ? < 9.0
// onprogress is not fired sometimes or delayed
// see also #64
xhr.onreadystatechange = onReadyStateChange
if ('contentType' in xhr) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 'padding=true'
}
xhr.open(method, url, true)
if ('readyState' in xhr) {
// workaround for Opera 12 issue with "progress" events
// #91
timeout = setTimeout(function () {
onTimeout()
}, 0)
}
}
XHRWrapper.prototype.abort = function () {
this._abort(false)
}
XHRWrapper.prototype.getResponseHeader = function (name) {
return this._contentType
}
XHRWrapper.prototype.setRequestHeader = function (name, value) {
var xhr = this._xhr
if ('setRequestHeader' in xhr) {
xhr.setRequestHeader(name, value)
}
}
XHRWrapper.prototype.getAllResponseHeaders = function () {
return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() : ''
}
XHRWrapper.prototype.send = function () {
// loading indicator in Safari < ? (6), Chrome < 14, Firefox
if (!('ontimeout' in XMLHttpRequest.prototype) &&
document != undefined &&
document.readyState != undefined &&
document.readyState !== 'complete') {
var that = this
that._sendTimeout = setTimeout(function () {
that._sendTimeout = 0
that.send()
}, 4)
return
}
var xhr = this._xhr
// withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
xhr.withCredentials = this.withCredentials
xhr.responseType = this.responseType
try {
// xhr.send(); throws "Not enough arguments" in Firefox 3.0
xhr.send(undefined)
} catch (error1) {
// Safari 5.1.7, Opera 12
throw error1
}
}
function toLowerCase (name) {
return name.replace(/[A-Z]/g, function (c) {
return String.fromCharCode(c.charCodeAt(0) + 0x20)
})
}
function HeadersPolyfill (all) {
// Get headers: implemented according to mozilla's example code: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders#Example
var map = Object.create(null)
var array = all.split('\r\n')
for (var i = 0; i < array.length; i += 1) {
var line = array[i]
var parts = line.split(': ')
var name = parts.shift()
var value = parts.join(': ')
map[toLowerCase(name)] = value
}
this._map = map
}
HeadersPolyfill.prototype.get = function (name) {
return this._map[toLowerCase(name)]
}
function XHRTransport () {
}
XHRTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
xhr.open('GET', url)
var offset = 0
xhr.onprogress = function () {
var responseText = xhr.responseText
var chunk = responseText.slice(offset)
offset += chunk.length
onProgressCallback(chunk)
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 2) {
var status = xhr.status
var statusText = xhr.statusText
var contentType = xhr.getResponseHeader('Content-Type')
var headers = xhr.getAllResponseHeaders()
onStartCallback(status, statusText, contentType, new HeadersPolyfill(headers), function () {
xhr.abort()
})
} else if (xhr.readyState === 4) {
onFinishCallback()
}
}
xhr.withCredentials = withCredentials
xhr.responseType = 'text'
for (var name in headers) {
if (Object.prototype.hasOwnProperty.call(headers, name)) {
xhr.setRequestHeader(name, headers[name])
}
}
xhr.send()
}
function HeadersWrapper (headers) {
this._headers = headers
}
HeadersWrapper.prototype.get = function (name) {
return this._headers.get(name)
}
function FetchTransport () {
}
FetchTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) {
var controller = new AbortController()
var signal = controller.signal// see #120
var textDecoder = new TextDecoder()
fetch(url, {
headers: headers,
credentials: withCredentials ? 'include' : 'same-origin',
signal: signal,
cache: 'no-store'
}).then(function (response) {
var reader = response.body.getReader()
onStartCallback(response.status, response.statusText, response.headers.get('Content-Type'), new HeadersWrapper(response.headers), function () {
controller.abort()
reader.cancel()
})
return new Promise(function (resolve, reject) {
var readNextChunk = function () {
reader.read().then(function (result) {
if (result.done) {
// Note: bytes in textDecoder are ignored
resolve(undefined)
} else {
var chunk = textDecoder.decode(result.value, { stream: true })
onProgressCallback(chunk)
readNextChunk()
}
})['catch'](function (error) {
reject(error)
})
}
readNextChunk()
})
}).then(function(result) {
onFinishCallback()
return result
}, function(error) {
onFinishCallback()
return Promise.reject(error)
})
}
function EventTarget () {
this._listeners = Object.create(null)
}
function throwError (e) {
setTimeout(function () {
throw e
}, 0)
}
EventTarget.prototype.dispatchEvent = function (event) {
event.target = this
var typeListeners = this._listeners[event.type]
if (typeListeners != undefined) {
var length = typeListeners.length
for (var i = 0; i < length; i += 1) {
var 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, listener) {
type = String(type)
var listeners = this._listeners
var typeListeners = listeners[type]
if (typeListeners == undefined) {
typeListeners = []
listeners[type] = typeListeners
}
var found = false
for (var i = 0; i < typeListeners.length; i += 1) {
if (typeListeners[i] === listener) {
found = true
}
}
if (!found) {
typeListeners.push(listener)
}
}
EventTarget.prototype.removeEventListener = function (type, listener) {
type = String(type)
var listeners = this._listeners
var typeListeners = listeners[type]
if (typeListeners != undefined) {
var filtered = []
for (var i = 0; i < typeListeners.length; i += 1) {
if (typeListeners[i] !== listener) {
filtered.push(typeListeners[i])
}
}
if (filtered.length === 0) {
delete listeners[type]
} else {
listeners[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 = Object.create(Event.prototype)
function ConnectionEvent (type, options) {
Event.call(this, type)
this.status = options.status
this.statusText = options.statusText
this.headers = options.headers
}
ConnectionEvent.prototype = Object.create(Event.prototype)
var WAITING = -1
var CONNECTING = 0
var OPEN = 1
var CLOSED = 2
var AFTER_CR = -1
var FIELD_START = 0
var FIELD = 1
var VALUE_START = 2
var VALUE = 3
var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i
var MINIMUM_DURATION = 1000
var MAXIMUM_DURATION = 18000000
var parseDuration = function (value, def) {
var n = parseInt(value, 10)
if (n !== n) {
n = def
}
return clampDuration(n)
}
var clampDuration = function (n) {
return Math.min(Math.max(n, MINIMUM_DURATION), MAXIMUM_DURATION)
}
var fire = function (that, f, event) {
try {
if (typeof f === 'function') {
f.call(that, event)
}
} catch (e) {
throwError(e)
}
}
function EventSourcePolyfill (url, options) {
EventTarget.call(this)
this.onopen = undefined
this.onmessage = undefined
this.onerror = undefined
this.url = undefined
this.readyState = undefined
this.withCredentials = undefined
this._close = undefined
start(this, url, options)
}
var isFetchSupported = fetch != undefined && Response != undefined && 'body' in Response.prototype
function start (es, url, options) {
url = String(url)
var withCredentials = options != undefined && Boolean(options.withCredentials)
var initialRetry = clampDuration(1000)
var heartbeatTimeout = options != undefined && options.heartbeatTimeout != undefined ? parseDuration(options.heartbeatTimeout, 45000) : clampDuration(45000)
var lastEventId = ''
var retry = initialRetry
var wasActivity = false
var headers = options != undefined && options.headers != undefined ? JSON.parse(JSON.stringify(options.headers)) : undefined
var CurrentTransport = options != undefined && options.Transport != undefined ? options.Transport : XMLHttpRequest
var xhr = isFetchSupported && !(options != undefined && options.Transport != undefined) ? undefined : new XHRWrapper(new CurrentTransport())
var transport = xhr == undefined ? new FetchTransport() : new XHRTransport()
var cancelFunction = undefined
var timeout = 0
var currentState = WAITING
var dataBuffer = ''
var lastEventIdBuffer = ''
var eventTypeBuffer = ''
var textBuffer = ''
var state = FIELD_START
var fieldStart = 0
var valueStart = 0
var onStart = function (status, statusText, contentType, headers, cancel) {
if (currentState === CONNECTING) {
cancelFunction = cancel
if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) {
currentState = OPEN
wasActivity = true
retry = initialRetry
es.readyState = OPEN
var event = new ConnectionEvent('open', {
status: status,
statusText: statusText,
headers: headers
})
es.dispatchEvent(event)
fire(es, es.onopen, event)
} else {
var message = ''
if (status !== 200) {
if (statusText) {
statusText = statusText.replace(/\s+/g, ' ')
}
message = "EventSource's response has a status " + status + ' ' + statusText + ' that is not 200. Aborting the connection.'
} else {
message = "EventSource's response has a Content-Type specifying an unsupported type: " + (contentType == undefined ? '-' : contentType.replace(/\s+/g, ' ')) + '. Aborting the connection.'
}
throwError(new Error(message))
close()
var event = new ConnectionEvent('error', {
status: status,
statusText: statusText,
headers: headers
})
es.dispatchEvent(event)
fire(es, es.onerror, event)
}
}
}
var onProgress = function (textChunk) {
if (currentState === OPEN) {
var n = -1
for (var i = 0; i < textChunk.length; i += 1) {
var c = textChunk.charCodeAt(i)
if (c === '\n'.charCodeAt(0) || c === '\r'.charCodeAt(0)) {
n = i
}
}
var chunk = (n !== -1 ? textBuffer : '') + textChunk.slice(0, n + 1)
textBuffer = (n === -1 ? textBuffer : '') + textChunk.slice(n + 1)
if (chunk !== '') {
wasActivity = true
}
for (var position = 0; position < chunk.length; position += 1) {
var c = chunk.charCodeAt(position)
if (state === AFTER_CR && c === '\n'.charCodeAt(0)) {
state = FIELD_START
} else {
if (state === AFTER_CR) {
state = FIELD_START
}
if (c === '\r'.charCodeAt(0) || c === '\n'.charCodeAt(0)) {
if (state !== FIELD_START) {
if (state === FIELD) {
valueStart = position + 1
}
var field = chunk.slice(fieldStart, valueStart - 1)
var value = chunk.slice(valueStart + (valueStart < position && chunk.charCodeAt(valueStart) === ' '.charCodeAt(0) ? 1 : 0), position)
if (field === 'data') {
dataBuffer += '\n'
dataBuffer += value
} else if (field === 'id') {
lastEventIdBuffer = value
} else if (field === 'event') {
eventTypeBuffer = value
} else if (field === 'retry') {
initialRetry = parseDuration(value, initialRetry)
retry = initialRetry
} else if (field === 'heartbeatTimeout') {
heartbeatTimeout = parseDuration(value, heartbeatTimeout)
if (timeout !== 0) {
clearTimeout(timeout)
timeout = setTimeout(function () {
onTimeout()
}, heartbeatTimeout)
}
}
}
if (state === FIELD_START) {
if (dataBuffer !== '') {
lastEventId = lastEventIdBuffer
if (eventTypeBuffer === '') {
eventTypeBuffer = 'message'
}
var event = new MessageEvent(eventTypeBuffer, {
data: dataBuffer.slice(1),
lastEventId: lastEventIdBuffer
})
es.dispatchEvent(event)
if (eventTypeBuffer === 'message') {
fire(es, es.onmessage, event)
}
if (currentState === CLOSED) {
return
}
}
dataBuffer = ''
eventTypeBuffer = ''
}
state = c === '\r'.charCodeAt(0) ? AFTER_CR : FIELD_START
} else {
if (state === FIELD_START) {
fieldStart = position
state = FIELD
}
if (state === FIELD) {
if (c === ':'.charCodeAt(0)) {
valueStart = position + 1
state = VALUE_START
}
} else if (state === VALUE_START) {
state = VALUE
}
}
}
}
}
}
var onFinish = function () {
if (currentState === OPEN || currentState === CONNECTING) {
currentState = WAITING
if (timeout !== 0) {
clearTimeout(timeout)
timeout = 0
}
timeout = setTimeout(function () {
onTimeout()
}, retry)
retry = clampDuration(Math.min(initialRetry * 16, retry * 2))
es.readyState = CONNECTING
var event = new Event('error')
es.dispatchEvent(event)
fire(es, es.onerror, event)
}
}
var close = function () {
currentState = CLOSED
if (cancelFunction != undefined) {
cancelFunction()
cancelFunction = undefined
}
if (timeout !== 0) {
clearTimeout(timeout)
timeout = 0
}
es.readyState = CLOSED
}
var onTimeout = function () {
timeout = 0
if (currentState !== WAITING) {
if (!wasActivity && cancelFunction != undefined) {
throwError(new Error('No activity within ' + heartbeatTimeout + ' milliseconds. Reconnecting.'))
cancelFunction()
cancelFunction = undefined
} else {
wasActivity = false
timeout = setTimeout(function () {
onTimeout()
}, heartbeatTimeout)
}
return
}
wasActivity = false
timeout = setTimeout(function () {
onTimeout()
}, heartbeatTimeout)
currentState = CONNECTING
dataBuffer = ''
eventTypeBuffer = ''
lastEventIdBuffer = lastEventId
textBuffer = ''
fieldStart = 0
valueStart = 0
state = FIELD_START
// https://bugzilla.mozilla.org/show_bug.cgi?id=428916
// Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers.
var requestURL = url
if (url.slice(0, 5) !== 'data:' && url.slice(0, 5) !== 'blob:') {
if (lastEventId !== '') {
requestURL += (url.indexOf('?') === -1 ? '?' : '&') + 'lastEventId=' + encodeURIComponent(lastEventId)
}
}
var requestHeaders = {}
requestHeaders['Accept'] = 'text/event-stream'
if (headers != undefined) {
for (var name in headers) {
if (Object.prototype.hasOwnProperty.call(headers, name)) {
requestHeaders[name] = headers[name]
}
}
}
try {
transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders)
} catch (error) {
close()
throw error
}
}
es.url = url
es.readyState = CONNECTING
es.withCredentials = withCredentials
es._close = close
onTimeout()
}
EventSourcePolyfill.prototype = Object.create(EventTarget.prototype)
EventSourcePolyfill.prototype.CONNECTING = CONNECTING
EventSourcePolyfill.prototype.OPEN = OPEN
EventSourcePolyfill.prototype.CLOSED = CLOSED
EventSourcePolyfill.prototype.close = function () {
this._close()
}
EventSourcePolyfill.CONNECTING = CONNECTING
EventSourcePolyfill.OPEN = OPEN
EventSourcePolyfill.CLOSED = CLOSED
EventSourcePolyfill.prototype.withCredentials = undefined
export default EventSourcePolyfill