Repository: tserkov/vue-sse Branch: v2 Commit: addad93db116 Files: 25 Total size: 184.5 KB Directory structure: gitextract_pn7ugql7/ ├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── dist/ │ ├── vue-sse.common.js │ ├── vue-sse.esm.browser.js │ ├── vue-sse.esm.js │ ├── vue-sse.js │ └── vue-sse.mjs ├── jest.config.js ├── package.json ├── rollup.config.js ├── src/ │ ├── index.cjs.js │ ├── index.js │ ├── sse-client.js │ └── sse-manager.js ├── test/ │ ├── .eslintrc.json │ ├── setup.js │ ├── sse-client.spec.js │ └── sse-manager.spec.js └── types/ ├── index.d.ts └── vue.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["@babel/preset-env"] } ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true [*.js] charset = utf-8 indent_style = space indent_size = 2 ================================================ FILE: .eslintrc.json ================================================ { "root": true, "extends": [ "plugin:vue/recommended" ] } ================================================ FILE: .gitignore ================================================ node_modules /lib # SublimeText *.sublime-project *.sublime-workspace ================================================ FILE: .prettierrc ================================================ { "printWidth": 120, "trailingComma": "all", "singleQuote": true } ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2021 James Churchard 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 ================================================ # VueSSE [![GitHub issues](https://img.shields.io/github/issues/tserkov/vue-sse.svg)]() [![license](https://img.shields.io/github/license/tserkov/vue-sse.svg)]() VueSSE enables effortless use of [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) by providing a high-level interface to an underlying [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource). ## Install ```bash # npm npm install --save vue-sse # OR yarn yarn add vue-sse ``` ```javascript // in main.js import VueSSE from 'vue-sse'; // using defaults Vue.use(VueSSE); // OR specify custom defaults (described below) Vue.use(VueSSE, { format: 'json', polyfill: true, url: '/my-events-server', withCredentials: true, }); ``` ## Quickstart ```js this.$sse.create('/my-events-server') .on('message', (msg) => console.info('Message:', msg)) .on('error', (err) => console.error('Failed to parse or lost connection:', err)) .connect() .catch((err) => console.error('Failed make initial connection:', err)); ``` ## Usage Clients can be created from the Vue object via `Vue.$sse.create(...)` or from within components via `this.$sse.create(...)` All of the following are valid calls to create a client: - `this.$sse.create('/your-events-endpoint')` to connect to the specified URL without specifying any config - `this.$sse.create({ url: '/your-events-endpoint', format: 'json' })` will automatically parse incoming messages as JSON - `this.$sse.create({ url: '/your-events-endpoint', withCredentials: true })` will set CORS on the request Once you've created a client, you can add handlers before or after calling `connect()`, which must be called. ## Configuration `$sse.create` accepts the following config options when installing VueSSE via `Vue.use` and when calling `$sse.create`. | Option | Type | Description | Default | | --- | --- | --- | -- | | format | `"plain"` \| `"json"` \| `(event: MessageEvent) => any` | Specify pre-processing, if any, to perform on incoming messages. Messages that fail formatting will emit an error. | `"plain"` | | url | `string` | The location of the SSE server. | `""` | | withCredentials | `boolean` | Indicates if CORS should be set to include credentials. | `false` | | polyfill | `boolean` | Include an [EventSource polyfill](https://github.com/Yaffle/EventSource) for older browsers. | `false` | | forcePolyfill | `boolean` | Forces the [EventSource polyfill](https://github.com/Yaffle/EventSource) to always be used over native. | `false` | | polyfillOptions | `object` | Custom options to provide to the [EventSource polyfill](https://github.com/Yaffle/EventSource#custom-headers). Only used if `forcePolyfill` is true. | `null` | If `$sse.create` is called with a string, it must be the URL to the SSE server. ## Methods Once you've successfully connected to an events server, a client will be returned with the following methods: | Name | Description | | --- | --- | | __connect__(): _Promise_ | Connects to the server. __Must be called.__ | | __on__(event: _string_, (data: _any_) => _void_): _SSEClient_ | Adds an event-specific listener to the event stream. The handler function receives the message as its argument (formatted if a format was specified), and the original underlying Event. For non-event messages, specify `""` or `"message"` as the event. | | __once__(event: _string_, (data: _any_) => _void_): _SSEClient_ | Same as `on(...)`, but only triggered once. | | __off__(event: _string_, (data: _any_ => _void_)): _SSEClient_ | Removes the given handler from the event stream. The function must be the same as provided to `on`/`once`. | | __on__('error', (err) => void): _SSEClient_ | Allows your application to handle any errors thrown, such as loss of server connection and pre-processing errors. | | __disconnect__(): _void_ | Closes the connection. The client can be re-used by calling `connect()`. __Must be called!__ (Usually, in the `beforeDestroy` of your component.) | ## Properties | Name | Type | Description | | --- | --- | --- | | source | `EventSource` | Returns the underlying EventSource. | ## Cleanup Every connection must be disconnected when the component is destroyed. There are two ways to achieve this: 1. Call `disconnect()` on the client during `beforeDestroy`, or 2. Add the following option to your component to have them automatically closed for you during `beforeDestroy`: ```js export default { name: 'my-component', data() { /* ... */ }, // ... sse: { cleanup: true, }, // ... } ``` ## Vue 3 This plugin works the same in both Vue 2 and 3. The Composition API is not yet supported. ## Example An example project is provided at [tserkov/vue-sse-example](https://github.com/tserkov/vue-sse-example). ### Kitchen Sink ```html ``` ================================================ FILE: dist/vue-sse.common.js ================================================ /*! * vue-sse v2.5.0 * (c) 2021 James Churchard * @license MIT */ 'use strict'; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } /** @license * eventsource.js * Available under MIT License (MIT) * https://github.com/Yaffle/EventSource/ */ var eventsource = createCommonjsModule(function (module, exports) { /*jslint indent: 2, vars: true, plusplus: true */ /*global setTimeout, clearTimeout */ (function (global) { var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; var XMLHttpRequest = global.XMLHttpRequest; var XDomainRequest = global.XDomainRequest; var ActiveXObject = global.ActiveXObject; var NativeEventSource = global.EventSource; var document = global.document; var Promise = global.Promise; var fetch = global.fetch; var Response = global.Response; var TextDecoder = global.TextDecoder; var TextEncoder = global.TextEncoder; var AbortController = global.AbortController; if (typeof window !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 document.readyState = "loading"; window.addEventListener("load", function (event) { document.readyState = "complete"; }, false); } if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 XMLHttpRequest = function () { return new ActiveXObject("Microsoft.XMLHTTP"); }; } if (Object.create == undefined) { Object.create = function (C) { function F(){} F.prototype = C; return new F(); }; } if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } // see #118 (Promise#finally with polyfilled Promise) // see #123 (data URLs crash Edge) // see #125 (CSP violations) // see pull/#138 // => No way to polyfill Promise#finally if (AbortController == undefined) { var originalFetch2 = fetch; fetch = function (url, options) { var signal = options.signal; return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { var reader = response.body.getReader(); signal._reader = reader; if (signal._aborted) { signal._reader.cancel(); } return { status: response.status, statusText: response.statusText, headers: response.headers, body: { getReader: function () { return reader; } } }; }); }; AbortController = function () { this.signal = { _reader: null, _aborted: false }; this.abort = function () { if (this.signal._reader != null) { this.signal._reader.cancel(); } this.signal._aborted = true; }; }; } 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.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); } return false; }; // IE, Edge if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { TextDecoder = TextDecoderPolyfill; } var k = function () { }; function XHRWrapper(xhr) { this.withCredentials = false; this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; this.onprogress = k; this.onload = k; this.onerror = 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.onabort(null); 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 (type, event) { if (event == null || event.preventDefault == null) { event = { preventDefault: k }; } // 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; if (type === "load") { that.onload(event); } else if (type === "error") { that.onerror(event); } else if (type === "abort") { that.onabort(event); } else { throw new TypeError(); } that.onreadystatechange(); } }; var onReadyStateChange = function (event) { if (xhr != undefined) { // Opera 12 if (xhr.readyState === 4) { if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { onFinish(xhr.responseText === "" ? "error" : "load", event); } } else if (xhr.readyState === 3) { if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 // and in Firefox 3.6 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 if ("onload" in xhr) { xhr.onload = function (event) { onFinish("load", event); }; } if ("onerror" in xhr) { xhr.onerror = function (event) { onFinish("error", event); }; } // 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 if ("onabort" in xhr) { xhr.onabort = function (event) { onFinish("abort", event); }; } if ("onprogress" in xhr) { 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 (significant lag in IE 11) if ("onreadystatechange" in xhr) { xhr.onreadystatechange = function (event) { onReadyStateChange(event); }; } if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; } xhr.open(method, url, true); if ("readyState" in xhr) { // workaround for Opera 12 issue with "progress" events // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) 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 () { // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; }; XHRWrapper.prototype.send = function () { // loading indicator in Safari < ? (6), Chrome < 14, Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" 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 ?) if ("withCredentials" in xhr) { xhr.withCredentials = this.withCredentials; } 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)]; }; if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 XMLHttpRequest.HEADERS_RECEIVED = 2; } 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.onerror = function (event) { event.preventDefault(); onFinishCallback(new Error("NetworkError")); }; xhr.onload = function () { onFinishCallback(null); }; xhr.onabort = function () { onFinishCallback(null); }; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { 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)); } }; xhr.withCredentials = withCredentials; for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { xhr.setRequestHeader(name, headers[name]); } } xhr.send(); return xhr; }; 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 reader = null; var controller = new AbortController(); var signal = controller.signal; var textDecoder = new TextDecoder(); fetch(url, { headers: headers, credentials: withCredentials ? "include" : "same-origin", signal: signal, cache: "no-store" }).then(function (response) { reader = response.body.getReader(); onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); // see https://github.com/promises-aplus/promises-spec/issues/179 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(); }); })["catch"](function (error) { if (error.name === "AbortError") { return undefined; } else { return error; } }).then(function (error) { onFinishCallback(error); }); return { abort: function () { if (reader != null) { reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 } controller.abort(); } }; }; 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); function ErrorEvent(type, options) { Event.call(this, type); this.error = options.error; } ErrorEvent.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(;.*)?$/i; var MINIMUM_DURATION = 1000; var MAXIMUM_DURATION = 18000000; var parseDuration = function (value, def) { var n = value == null ? def : 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); options = options || {}; this.onopen = undefined; this.onmessage = undefined; this.onerror = undefined; this.url = undefined; this.readyState = undefined; this.withCredentials = undefined; this.headers = undefined; this._close = undefined; start(this, url, options); } function getBestXHRTransport() { return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined ? new XMLHttpRequest() : new XDomainRequest(); } var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; function start(es, url, options) { url = String(url); var withCredentials = Boolean(options.withCredentials); var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; var initialRetry = clampDuration(1000); var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); var lastEventId = ""; var retry = initialRetry; var wasActivity = false; var textLength = 0; var headers = options.headers || {}; var TransportOption = options.Transport; var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); var abortController = 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) { if (currentState === CONNECTING) { if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { currentState = OPEN; wasActivity = Date.now(); 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."; } close(); var event = new ConnectionEvent("error", { status: status, statusText: statusText, headers: headers }); es.dispatchEvent(event); fire(es, es.onerror, event); console.error(message); } } }; 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 (textChunk !== "") { wasActivity = Date.now(); textLength += textChunk.length; } 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 === "open") { fire(es, es.onopen, event); } else if (eventTypeBuffer === "message") { fire(es, es.onmessage, event); } else if (eventTypeBuffer === "error") { fire(es, es.onerror, 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 (error) { 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 ErrorEvent("error", {error: error}); es.dispatchEvent(event); fire(es, es.onerror, event); if (error != undefined) { console.error(error); } } }; var close = function () { currentState = CLOSED; if (abortController != undefined) { abortController.abort(); abortController = undefined; } if (timeout !== 0) { clearTimeout(timeout); timeout = 0; } es.readyState = CLOSED; }; var onTimeout = function () { timeout = 0; if (currentState !== WAITING) { if (!wasActivity && abortController != undefined) { onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); if (abortController != undefined) { abortController.abort(); abortController = undefined; } } else { var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); wasActivity = false; timeout = setTimeout(function () { onTimeout(); }, nextHeartbeat); } return; } wasActivity = false; textLength = 0; 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 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); } } var withCredentials = es.withCredentials; var requestHeaders = {}; requestHeaders["Accept"] = "text/event-stream"; var headers = es.headers; if (headers != undefined) { for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { requestHeaders[name] = headers[name]; } } } try { abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); } catch (error) { close(); throw error; } }; es.url = url; es.readyState = CONNECTING; es.withCredentials = withCredentials; es.headers = headers; 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; var R = NativeEventSource; if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { // 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 // ... R = EventSourcePolyfill; } (function (factory) { { var v = factory(exports); if (v !== undefined) { module.exports = v; } } })(function (exports) { exports.EventSourcePolyfill = EventSourcePolyfill; exports.NativeEventSource = NativeEventSource; exports.EventSource = R; }); }(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal) : globalThis)); }); var eventsource$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign(/*#__PURE__*/Object.create(null), eventsource, { 'default': eventsource })); var formatText = function (e) { return e.data; }; var formatJSON = function (e) { return JSON.parse(e.data); }; var SSEClient = function SSEClient(config) { this._handlers = {}; this._listeners = {}; this._source = null; if (config.format) { if (typeof config.format === 'string') { if (config.format === 'plain') { this._format = formatText; } else if (config.format === 'json') { this._format = formatJSON; } else { this._format = formatText; } } else if (typeof config.format === 'function') { this._format = config.format; } else { this._format = formatText; } } else { this._format = formatText; } if (config.handlers) { for (var event in config.handlers) { this.on(event, config.handlers[event]); } } this.url = config.url; this.withCredentials = !!config.withCredentials; this.polyfillOptions = config.polyfillOptions || {}; this.forcePolyfill = !!config.forcePolyfill; }; var prototypeAccessors = { source: { configurable: true } }; prototypeAccessors.source.get = function () { return this._source; }; SSEClient.prototype.connect = function connect () { var this$1 = this; if (this.forcePolyfill) { this._source = new eventsource.EventSourcePolyfill( this.url, Object.assign({}, this.polyfillOptions, { withCredentials: this.withCredentials, }) ); } else { this._source = new window.EventSource(this.url, { withCredentials: this.withCredentials, }); } return new Promise(function (resolve, reject) { this$1._source.onopen = function () { // Add event listeners that were added before we connected for (var event in this$1._listeners) { this$1._source.addEventListener(event, this$1._listeners[event]); } this$1._source.onerror = null; resolve(this$1); }; this$1._source.onerror = reject; }); }; SSEClient.prototype.disconnect = function disconnect () { if (this._source !== null) { this._source.close(); this._source = null; } }; SSEClient.prototype.on = function on (event, handler) { if (!event) { // Default "event-less" event event = 'message'; } if (!this._listeners[event]) { this._create(event); } this._handlers[event].push(handler); return this; }; SSEClient.prototype.once = function once (event, handler) { var this$1 = this; this.on(event, function (e) { this$1.off(event, handler); handler(e); }); return this; }; SSEClient.prototype.off = function off (event, handler) { if (!this._handlers[event]) { // no handlers registered for event return this; } var idx = this._handlers[event].indexOf(handler); if (idx === -1) { // handler not registered for event return this; } // remove handler from event this._handlers[event].splice(idx, 1); if (this._handlers[event].length === 0) { // remove listener since no handlers exist this._source.removeEventListener(event, this._listeners[event]); delete this._handlers[event]; delete this._listeners[event]; } return this; }; SSEClient.prototype._create = function _create (event) { var this$1 = this; this._handlers[event] = []; this._listeners[event] = function (message) { var data; try { data = this$1._format(message); } catch (err) { if (typeof this$1._source.onerror === 'function') { this$1._source.onerror(err); } return; } this$1._handlers[event].forEach(function (handler) { return handler(data, message.lastEventId); }); }; if (this._source) { this._source.addEventListener(event, this._listeners[event]); } }; Object.defineProperties( SSEClient.prototype, prototypeAccessors ); function install(Vue, config) { if (Vue.config && Vue.config.globalProperties) { // Vue3 Vue.config.globalProperties.$sse = new SSEManager(config); } else { // Vue2 // eslint-disable-next-line no-param-reassign, no-multi-assign Vue.$sse = Vue.prototype.$sse = new SSEManager(config); } if (config && config.polyfill) { Promise.resolve().then(function () { return eventsource$1; }); } // This mixin allows components to specify that all clients that were // created within it should be automatically disconnected (cleanup) // when the component is destroyed. Vue.mixin({ beforeCreate: function beforeCreate() { if (this.$options.sse && this.$options.sse.cleanup) { // We instantiate an SSEManager for this specific instance // in order to track it (see discussions in #13 for rationale). this.$sse = new SSEManager(); // We also set $clients to an empty array, as opposed to null, // so that beforeDestroy and create know to use it. this.$sse.$clients = []; } }, beforeDestroy: function beforeDestroy() { if (this.$sse.$clients !== null) { this.$sse.$clients.forEach(function (c) { return c.disconnect(); }); this.$sse.$clients = []; } }, }); } var SSEManager = function SSEManager(config) { this.$defaultConfig = Object.assign( { format: formatText, sendCredentials: false, }, config ); this.$clients = null; }; SSEManager.prototype.create = function create (configOrURL) { var config; if (typeof configOrURL === 'object') { config = configOrURL; } else if (typeof configOrURL === 'string') { config = { url: configOrURL, }; } else { config = {}; } var client = new SSEClient(Object.assign({}, this.$defaultConfig, config)); // If $clients is not null, then it's array that we should push this // client into for later cleanup in our mixin's beforeDestroy. if (this.$clients !== null) { this.$clients.push(client); } return client; }; var index_cjs = { SSEManager: SSEManager, install: install, }; module.exports = index_cjs; ================================================ FILE: dist/vue-sse.esm.browser.js ================================================ /*! * vue-sse v2.5.0 * (c) 2021 James Churchard * @license MIT */ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } /** @license * eventsource.js * Available under MIT License (MIT) * https://github.com/Yaffle/EventSource/ */ var eventsource = createCommonjsModule(function (module, exports) { /*jslint indent: 2, vars: true, plusplus: true */ /*global setTimeout, clearTimeout */ (function (global) { var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; var XMLHttpRequest = global.XMLHttpRequest; var XDomainRequest = global.XDomainRequest; var ActiveXObject = global.ActiveXObject; var NativeEventSource = global.EventSource; var document = global.document; var Promise = global.Promise; var fetch = global.fetch; var Response = global.Response; var TextDecoder = global.TextDecoder; var TextEncoder = global.TextEncoder; var AbortController = global.AbortController; if (typeof window !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 document.readyState = "loading"; window.addEventListener("load", function (event) { document.readyState = "complete"; }, false); } if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 XMLHttpRequest = function () { return new ActiveXObject("Microsoft.XMLHTTP"); }; } if (Object.create == undefined) { Object.create = function (C) { function F(){} F.prototype = C; return new F(); }; } if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } // see #118 (Promise#finally with polyfilled Promise) // see #123 (data URLs crash Edge) // see #125 (CSP violations) // see pull/#138 // => No way to polyfill Promise#finally if (AbortController == undefined) { var originalFetch2 = fetch; fetch = function (url, options) { var signal = options.signal; return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { var reader = response.body.getReader(); signal._reader = reader; if (signal._aborted) { signal._reader.cancel(); } return { status: response.status, statusText: response.statusText, headers: response.headers, body: { getReader: function () { return reader; } } }; }); }; AbortController = function () { this.signal = { _reader: null, _aborted: false }; this.abort = function () { if (this.signal._reader != null) { this.signal._reader.cancel(); } this.signal._aborted = true; }; }; } 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.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); } return false; }; // IE, Edge if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { TextDecoder = TextDecoderPolyfill; } var k = function () { }; function XHRWrapper(xhr) { this.withCredentials = false; this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; this.onprogress = k; this.onload = k; this.onerror = 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.onabort(null); 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 (type, event) { if (event == null || event.preventDefault == null) { event = { preventDefault: k }; } // 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; if (type === "load") { that.onload(event); } else if (type === "error") { that.onerror(event); } else if (type === "abort") { that.onabort(event); } else { throw new TypeError(); } that.onreadystatechange(); } }; var onReadyStateChange = function (event) { if (xhr != undefined) { // Opera 12 if (xhr.readyState === 4) { if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { onFinish(xhr.responseText === "" ? "error" : "load", event); } } else if (xhr.readyState === 3) { if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 // and in Firefox 3.6 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 if ("onload" in xhr) { xhr.onload = function (event) { onFinish("load", event); }; } if ("onerror" in xhr) { xhr.onerror = function (event) { onFinish("error", event); }; } // 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 if ("onabort" in xhr) { xhr.onabort = function (event) { onFinish("abort", event); }; } if ("onprogress" in xhr) { 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 (significant lag in IE 11) if ("onreadystatechange" in xhr) { xhr.onreadystatechange = function (event) { onReadyStateChange(event); }; } if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; } xhr.open(method, url, true); if ("readyState" in xhr) { // workaround for Opera 12 issue with "progress" events // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) 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 () { // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; }; XHRWrapper.prototype.send = function () { // loading indicator in Safari < ? (6), Chrome < 14, Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" 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 ?) if ("withCredentials" in xhr) { xhr.withCredentials = this.withCredentials; } 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)]; }; if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 XMLHttpRequest.HEADERS_RECEIVED = 2; } 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.onerror = function (event) { event.preventDefault(); onFinishCallback(new Error("NetworkError")); }; xhr.onload = function () { onFinishCallback(null); }; xhr.onabort = function () { onFinishCallback(null); }; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { 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)); } }; xhr.withCredentials = withCredentials; for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { xhr.setRequestHeader(name, headers[name]); } } xhr.send(); return xhr; }; 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 reader = null; var controller = new AbortController(); var signal = controller.signal; var textDecoder = new TextDecoder(); fetch(url, { headers: headers, credentials: withCredentials ? "include" : "same-origin", signal: signal, cache: "no-store" }).then(function (response) { reader = response.body.getReader(); onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); // see https://github.com/promises-aplus/promises-spec/issues/179 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(); }); })["catch"](function (error) { if (error.name === "AbortError") { return undefined; } else { return error; } }).then(function (error) { onFinishCallback(error); }); return { abort: function () { if (reader != null) { reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 } controller.abort(); } }; }; 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); function ErrorEvent(type, options) { Event.call(this, type); this.error = options.error; } ErrorEvent.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(;.*)?$/i; var MINIMUM_DURATION = 1000; var MAXIMUM_DURATION = 18000000; var parseDuration = function (value, def) { var n = value == null ? def : 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); options = options || {}; this.onopen = undefined; this.onmessage = undefined; this.onerror = undefined; this.url = undefined; this.readyState = undefined; this.withCredentials = undefined; this.headers = undefined; this._close = undefined; start(this, url, options); } function getBestXHRTransport() { return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined ? new XMLHttpRequest() : new XDomainRequest(); } var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; function start(es, url, options) { url = String(url); var withCredentials = Boolean(options.withCredentials); var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; var initialRetry = clampDuration(1000); var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); var lastEventId = ""; var retry = initialRetry; var wasActivity = false; var textLength = 0; var headers = options.headers || {}; var TransportOption = options.Transport; var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); var abortController = 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) { if (currentState === CONNECTING) { if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { currentState = OPEN; wasActivity = Date.now(); 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."; } close(); var event = new ConnectionEvent("error", { status: status, statusText: statusText, headers: headers }); es.dispatchEvent(event); fire(es, es.onerror, event); console.error(message); } } }; 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 (textChunk !== "") { wasActivity = Date.now(); textLength += textChunk.length; } 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 === "open") { fire(es, es.onopen, event); } else if (eventTypeBuffer === "message") { fire(es, es.onmessage, event); } else if (eventTypeBuffer === "error") { fire(es, es.onerror, 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 (error) { 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 ErrorEvent("error", {error: error}); es.dispatchEvent(event); fire(es, es.onerror, event); if (error != undefined) { console.error(error); } } }; var close = function () { currentState = CLOSED; if (abortController != undefined) { abortController.abort(); abortController = undefined; } if (timeout !== 0) { clearTimeout(timeout); timeout = 0; } es.readyState = CLOSED; }; var onTimeout = function () { timeout = 0; if (currentState !== WAITING) { if (!wasActivity && abortController != undefined) { onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); if (abortController != undefined) { abortController.abort(); abortController = undefined; } } else { var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); wasActivity = false; timeout = setTimeout(function () { onTimeout(); }, nextHeartbeat); } return; } wasActivity = false; textLength = 0; 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 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); } } var withCredentials = es.withCredentials; var requestHeaders = {}; requestHeaders["Accept"] = "text/event-stream"; var headers = es.headers; if (headers != undefined) { for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { requestHeaders[name] = headers[name]; } } } try { abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); } catch (error) { close(); throw error; } }; es.url = url; es.readyState = CONNECTING; es.withCredentials = withCredentials; es.headers = headers; 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; var R = NativeEventSource; if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { // 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 // ... R = EventSourcePolyfill; } (function (factory) { { var v = factory(exports); if (v !== undefined) module.exports = v; } })(function (exports) { exports.EventSourcePolyfill = EventSourcePolyfill; exports.NativeEventSource = NativeEventSource; exports.EventSource = R; }); }(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal) : globalThis)); }); var eventsource$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign(/*#__PURE__*/Object.create(null), eventsource, { 'default': eventsource })); const formatText = (e) => e.data; const formatJSON = (e) => JSON.parse(e.data); class SSEClient { constructor(config) { this._handlers = {}; this._listeners = {}; this._source = null; if (config.format) { if (typeof config.format === 'string') { if (config.format === 'plain') { this._format = formatText; } else if (config.format === 'json') { this._format = formatJSON; } else { this._format = formatText; } } else if (typeof config.format === 'function') { this._format = config.format; } else { this._format = formatText; } } else { this._format = formatText; } if (config.handlers) { for (const event in config.handlers) { this.on(event, config.handlers[event]); } } this.url = config.url; this.withCredentials = !!config.withCredentials; this.polyfillOptions = config.polyfillOptions || {}; this.forcePolyfill = !!config.forcePolyfill; } get source() { return this._source; } connect() { if (this.forcePolyfill) { this._source = new eventsource.EventSourcePolyfill( this.url, Object.assign({}, this.polyfillOptions, { withCredentials: this.withCredentials, }), ); } else { this._source = new window.EventSource(this.url, { withCredentials: this.withCredentials, }); } return new Promise((resolve, reject) => { this._source.onopen = () => { // Add event listeners that were added before we connected for (let event in this._listeners) { this._source.addEventListener(event, this._listeners[event]); } this._source.onerror = null; resolve(this); }; this._source.onerror = reject; }); } disconnect() { if (this._source !== null) { this._source.close(); this._source = null; } } on(event, handler) { if (!event) { // Default "event-less" event event = 'message'; } if (!this._listeners[event]) { this._create(event); } this._handlers[event].push(handler); return this; } once(event, handler) { this.on(event, (e) => { this.off(event, handler); handler(e); }); return this; } off(event, handler) { if (!this._handlers[event]) { // no handlers registered for event return this; } const idx = this._handlers[event].indexOf(handler); if (idx === -1) { // handler not registered for event return this; } // remove handler from event this._handlers[event].splice(idx, 1); if (this._handlers[event].length === 0) { // remove listener since no handlers exist this._source.removeEventListener(event, this._listeners[event]); delete this._handlers[event]; delete this._listeners[event]; } return this; } _create(event) { this._handlers[event] = []; this._listeners[event] = (message) => { let data; try { data = this._format(message); } catch (err) { if (typeof this._source.onerror === 'function') { this._source.onerror(err); } return; } this._handlers[event].forEach((handler) => handler(data, message.lastEventId)); }; if (this._source) { this._source.addEventListener(event, this._listeners[event]); } } } function install(Vue, config) { if (Vue.config && Vue.config.globalProperties) { // Vue3 Vue.config.globalProperties.$sse = new SSEManager(config); } else { // Vue2 // eslint-disable-next-line no-param-reassign, no-multi-assign Vue.$sse = Vue.prototype.$sse = new SSEManager(config); } if (config && config.polyfill) { Promise.resolve().then(function () { return eventsource$1; }); } // This mixin allows components to specify that all clients that were // created within it should be automatically disconnected (cleanup) // when the component is destroyed. Vue.mixin({ beforeCreate() { if (this.$options.sse && this.$options.sse.cleanup) { // We instantiate an SSEManager for this specific instance // in order to track it (see discussions in #13 for rationale). this.$sse = new SSEManager(); // We also set $clients to an empty array, as opposed to null, // so that beforeDestroy and create know to use it. this.$sse.$clients = []; } }, beforeDestroy() { if (this.$sse.$clients !== null) { this.$sse.$clients.forEach((c) => c.disconnect()); this.$sse.$clients = []; } }, }); } class SSEManager { constructor(config) { this.$defaultConfig = Object.assign( { format: formatText, sendCredentials: false, }, config, ); this.$clients = null; } create(configOrURL) { let config; if (typeof configOrURL === 'object') { config = configOrURL; } else if (typeof configOrURL === 'string') { config = { url: configOrURL, }; } else { config = {}; } const client = new SSEClient(Object.assign({}, this.$defaultConfig, config)); // If $clients is not null, then it's array that we should push this // client into for later cleanup in our mixin's beforeDestroy. if (this.$clients !== null) { this.$clients.push(client); } return client; } } var index = { install, }; export default index; export { SSEManager, install }; ================================================ FILE: dist/vue-sse.esm.js ================================================ /*! * vue-sse v2.5.0 * (c) 2021 James Churchard * @license MIT */ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } /** @license * eventsource.js * Available under MIT License (MIT) * https://github.com/Yaffle/EventSource/ */ var eventsource = createCommonjsModule(function (module, exports) { /*jslint indent: 2, vars: true, plusplus: true */ /*global setTimeout, clearTimeout */ (function (global) { var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; var XMLHttpRequest = global.XMLHttpRequest; var XDomainRequest = global.XDomainRequest; var ActiveXObject = global.ActiveXObject; var NativeEventSource = global.EventSource; var document = global.document; var Promise = global.Promise; var fetch = global.fetch; var Response = global.Response; var TextDecoder = global.TextDecoder; var TextEncoder = global.TextEncoder; var AbortController = global.AbortController; if (typeof window !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 document.readyState = "loading"; window.addEventListener("load", function (event) { document.readyState = "complete"; }, false); } if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 XMLHttpRequest = function () { return new ActiveXObject("Microsoft.XMLHTTP"); }; } if (Object.create == undefined) { Object.create = function (C) { function F(){} F.prototype = C; return new F(); }; } if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } // see #118 (Promise#finally with polyfilled Promise) // see #123 (data URLs crash Edge) // see #125 (CSP violations) // see pull/#138 // => No way to polyfill Promise#finally if (AbortController == undefined) { var originalFetch2 = fetch; fetch = function (url, options) { var signal = options.signal; return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { var reader = response.body.getReader(); signal._reader = reader; if (signal._aborted) { signal._reader.cancel(); } return { status: response.status, statusText: response.statusText, headers: response.headers, body: { getReader: function () { return reader; } } }; }); }; AbortController = function () { this.signal = { _reader: null, _aborted: false }; this.abort = function () { if (this.signal._reader != null) { this.signal._reader.cancel(); } this.signal._aborted = true; }; }; } 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.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); } return false; }; // IE, Edge if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { TextDecoder = TextDecoderPolyfill; } var k = function () { }; function XHRWrapper(xhr) { this.withCredentials = false; this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; this.onprogress = k; this.onload = k; this.onerror = 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.onabort(null); 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 (type, event) { if (event == null || event.preventDefault == null) { event = { preventDefault: k }; } // 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; if (type === "load") { that.onload(event); } else if (type === "error") { that.onerror(event); } else if (type === "abort") { that.onabort(event); } else { throw new TypeError(); } that.onreadystatechange(); } }; var onReadyStateChange = function (event) { if (xhr != undefined) { // Opera 12 if (xhr.readyState === 4) { if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { onFinish(xhr.responseText === "" ? "error" : "load", event); } } else if (xhr.readyState === 3) { if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 // and in Firefox 3.6 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 if ("onload" in xhr) { xhr.onload = function (event) { onFinish("load", event); }; } if ("onerror" in xhr) { xhr.onerror = function (event) { onFinish("error", event); }; } // 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 if ("onabort" in xhr) { xhr.onabort = function (event) { onFinish("abort", event); }; } if ("onprogress" in xhr) { 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 (significant lag in IE 11) if ("onreadystatechange" in xhr) { xhr.onreadystatechange = function (event) { onReadyStateChange(event); }; } if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; } xhr.open(method, url, true); if ("readyState" in xhr) { // workaround for Opera 12 issue with "progress" events // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) 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 () { // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; }; XHRWrapper.prototype.send = function () { // loading indicator in Safari < ? (6), Chrome < 14, Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" 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 ?) if ("withCredentials" in xhr) { xhr.withCredentials = this.withCredentials; } 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)]; }; if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 XMLHttpRequest.HEADERS_RECEIVED = 2; } 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.onerror = function (event) { event.preventDefault(); onFinishCallback(new Error("NetworkError")); }; xhr.onload = function () { onFinishCallback(null); }; xhr.onabort = function () { onFinishCallback(null); }; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { 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)); } }; xhr.withCredentials = withCredentials; for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { xhr.setRequestHeader(name, headers[name]); } } xhr.send(); return xhr; }; 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 reader = null; var controller = new AbortController(); var signal = controller.signal; var textDecoder = new TextDecoder(); fetch(url, { headers: headers, credentials: withCredentials ? "include" : "same-origin", signal: signal, cache: "no-store" }).then(function (response) { reader = response.body.getReader(); onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); // see https://github.com/promises-aplus/promises-spec/issues/179 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(); }); })["catch"](function (error) { if (error.name === "AbortError") { return undefined; } else { return error; } }).then(function (error) { onFinishCallback(error); }); return { abort: function () { if (reader != null) { reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 } controller.abort(); } }; }; 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); function ErrorEvent(type, options) { Event.call(this, type); this.error = options.error; } ErrorEvent.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(;.*)?$/i; var MINIMUM_DURATION = 1000; var MAXIMUM_DURATION = 18000000; var parseDuration = function (value, def) { var n = value == null ? def : 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); options = options || {}; this.onopen = undefined; this.onmessage = undefined; this.onerror = undefined; this.url = undefined; this.readyState = undefined; this.withCredentials = undefined; this.headers = undefined; this._close = undefined; start(this, url, options); } function getBestXHRTransport() { return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined ? new XMLHttpRequest() : new XDomainRequest(); } var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; function start(es, url, options) { url = String(url); var withCredentials = Boolean(options.withCredentials); var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; var initialRetry = clampDuration(1000); var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); var lastEventId = ""; var retry = initialRetry; var wasActivity = false; var textLength = 0; var headers = options.headers || {}; var TransportOption = options.Transport; var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); var abortController = 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) { if (currentState === CONNECTING) { if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { currentState = OPEN; wasActivity = Date.now(); 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."; } close(); var event = new ConnectionEvent("error", { status: status, statusText: statusText, headers: headers }); es.dispatchEvent(event); fire(es, es.onerror, event); console.error(message); } } }; 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 (textChunk !== "") { wasActivity = Date.now(); textLength += textChunk.length; } 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 === "open") { fire(es, es.onopen, event); } else if (eventTypeBuffer === "message") { fire(es, es.onmessage, event); } else if (eventTypeBuffer === "error") { fire(es, es.onerror, 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 (error) { 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 ErrorEvent("error", {error: error}); es.dispatchEvent(event); fire(es, es.onerror, event); if (error != undefined) { console.error(error); } } }; var close = function () { currentState = CLOSED; if (abortController != undefined) { abortController.abort(); abortController = undefined; } if (timeout !== 0) { clearTimeout(timeout); timeout = 0; } es.readyState = CLOSED; }; var onTimeout = function () { timeout = 0; if (currentState !== WAITING) { if (!wasActivity && abortController != undefined) { onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); if (abortController != undefined) { abortController.abort(); abortController = undefined; } } else { var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); wasActivity = false; timeout = setTimeout(function () { onTimeout(); }, nextHeartbeat); } return; } wasActivity = false; textLength = 0; 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 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); } } var withCredentials = es.withCredentials; var requestHeaders = {}; requestHeaders["Accept"] = "text/event-stream"; var headers = es.headers; if (headers != undefined) { for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { requestHeaders[name] = headers[name]; } } } try { abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); } catch (error) { close(); throw error; } }; es.url = url; es.readyState = CONNECTING; es.withCredentials = withCredentials; es.headers = headers; 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; var R = NativeEventSource; if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { // 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 // ... R = EventSourcePolyfill; } (function (factory) { { var v = factory(exports); if (v !== undefined) { module.exports = v; } } })(function (exports) { exports.EventSourcePolyfill = EventSourcePolyfill; exports.NativeEventSource = NativeEventSource; exports.EventSource = R; }); }(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal) : globalThis)); }); var eventsource$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign(/*#__PURE__*/Object.create(null), eventsource, { 'default': eventsource })); var formatText = function (e) { return e.data; }; var formatJSON = function (e) { return JSON.parse(e.data); }; var SSEClient = function SSEClient(config) { this._handlers = {}; this._listeners = {}; this._source = null; if (config.format) { if (typeof config.format === 'string') { if (config.format === 'plain') { this._format = formatText; } else if (config.format === 'json') { this._format = formatJSON; } else { this._format = formatText; } } else if (typeof config.format === 'function') { this._format = config.format; } else { this._format = formatText; } } else { this._format = formatText; } if (config.handlers) { for (var event in config.handlers) { this.on(event, config.handlers[event]); } } this.url = config.url; this.withCredentials = !!config.withCredentials; this.polyfillOptions = config.polyfillOptions || {}; this.forcePolyfill = !!config.forcePolyfill; }; var prototypeAccessors = { source: { configurable: true } }; prototypeAccessors.source.get = function () { return this._source; }; SSEClient.prototype.connect = function connect () { var this$1 = this; if (this.forcePolyfill) { this._source = new eventsource.EventSourcePolyfill( this.url, Object.assign({}, this.polyfillOptions, { withCredentials: this.withCredentials, }) ); } else { this._source = new window.EventSource(this.url, { withCredentials: this.withCredentials, }); } return new Promise(function (resolve, reject) { this$1._source.onopen = function () { // Add event listeners that were added before we connected for (var event in this$1._listeners) { this$1._source.addEventListener(event, this$1._listeners[event]); } this$1._source.onerror = null; resolve(this$1); }; this$1._source.onerror = reject; }); }; SSEClient.prototype.disconnect = function disconnect () { if (this._source !== null) { this._source.close(); this._source = null; } }; SSEClient.prototype.on = function on (event, handler) { if (!event) { // Default "event-less" event event = 'message'; } if (!this._listeners[event]) { this._create(event); } this._handlers[event].push(handler); return this; }; SSEClient.prototype.once = function once (event, handler) { var this$1 = this; this.on(event, function (e) { this$1.off(event, handler); handler(e); }); return this; }; SSEClient.prototype.off = function off (event, handler) { if (!this._handlers[event]) { // no handlers registered for event return this; } var idx = this._handlers[event].indexOf(handler); if (idx === -1) { // handler not registered for event return this; } // remove handler from event this._handlers[event].splice(idx, 1); if (this._handlers[event].length === 0) { // remove listener since no handlers exist this._source.removeEventListener(event, this._listeners[event]); delete this._handlers[event]; delete this._listeners[event]; } return this; }; SSEClient.prototype._create = function _create (event) { var this$1 = this; this._handlers[event] = []; this._listeners[event] = function (message) { var data; try { data = this$1._format(message); } catch (err) { if (typeof this$1._source.onerror === 'function') { this$1._source.onerror(err); } return; } this$1._handlers[event].forEach(function (handler) { return handler(data, message.lastEventId); }); }; if (this._source) { this._source.addEventListener(event, this._listeners[event]); } }; Object.defineProperties( SSEClient.prototype, prototypeAccessors ); function install(Vue, config) { if (Vue.config && Vue.config.globalProperties) { // Vue3 Vue.config.globalProperties.$sse = new SSEManager(config); } else { // Vue2 // eslint-disable-next-line no-param-reassign, no-multi-assign Vue.$sse = Vue.prototype.$sse = new SSEManager(config); } if (config && config.polyfill) { Promise.resolve().then(function () { return eventsource$1; }); } // This mixin allows components to specify that all clients that were // created within it should be automatically disconnected (cleanup) // when the component is destroyed. Vue.mixin({ beforeCreate: function beforeCreate() { if (this.$options.sse && this.$options.sse.cleanup) { // We instantiate an SSEManager for this specific instance // in order to track it (see discussions in #13 for rationale). this.$sse = new SSEManager(); // We also set $clients to an empty array, as opposed to null, // so that beforeDestroy and create know to use it. this.$sse.$clients = []; } }, beforeDestroy: function beforeDestroy() { if (this.$sse.$clients !== null) { this.$sse.$clients.forEach(function (c) { return c.disconnect(); }); this.$sse.$clients = []; } }, }); } var SSEManager = function SSEManager(config) { this.$defaultConfig = Object.assign( { format: formatText, sendCredentials: false, }, config ); this.$clients = null; }; SSEManager.prototype.create = function create (configOrURL) { var config; if (typeof configOrURL === 'object') { config = configOrURL; } else if (typeof configOrURL === 'string') { config = { url: configOrURL, }; } else { config = {}; } var client = new SSEClient(Object.assign({}, this.$defaultConfig, config)); // If $clients is not null, then it's array that we should push this // client into for later cleanup in our mixin's beforeDestroy. if (this.$clients !== null) { this.$clients.push(client); } return client; }; var index = { install: install, }; export default index; export { SSEManager, install }; ================================================ FILE: dist/vue-sse.js ================================================ /*! * vue-sse v2.5.0 * (c) 2021 James Churchard * @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.VueSSE = factory()); }(this, (function () { 'use strict'; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } /** @license * eventsource.js * Available under MIT License (MIT) * https://github.com/Yaffle/EventSource/ */ var eventsource = createCommonjsModule(function (module, exports) { /*jslint indent: 2, vars: true, plusplus: true */ /*global setTimeout, clearTimeout */ (function (global) { var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; var XMLHttpRequest = global.XMLHttpRequest; var XDomainRequest = global.XDomainRequest; var ActiveXObject = global.ActiveXObject; var NativeEventSource = global.EventSource; var document = global.document; var Promise = global.Promise; var fetch = global.fetch; var Response = global.Response; var TextDecoder = global.TextDecoder; var TextEncoder = global.TextEncoder; var AbortController = global.AbortController; if (typeof window !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 document.readyState = "loading"; window.addEventListener("load", function (event) { document.readyState = "complete"; }, false); } if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 XMLHttpRequest = function () { return new ActiveXObject("Microsoft.XMLHTTP"); }; } if (Object.create == undefined) { Object.create = function (C) { function F(){} F.prototype = C; return new F(); }; } if (!Date.now) { Date.now = function now() { return new Date().getTime(); }; } // see #118 (Promise#finally with polyfilled Promise) // see #123 (data URLs crash Edge) // see #125 (CSP violations) // see pull/#138 // => No way to polyfill Promise#finally if (AbortController == undefined) { var originalFetch2 = fetch; fetch = function (url, options) { var signal = options.signal; return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { var reader = response.body.getReader(); signal._reader = reader; if (signal._aborted) { signal._reader.cancel(); } return { status: response.status, statusText: response.statusText, headers: response.headers, body: { getReader: function () { return reader; } } }; }); }; AbortController = function () { this.signal = { _reader: null, _aborted: false }; this.abort = function () { if (this.signal._reader != null) { this.signal._reader.cancel(); } this.signal._aborted = true; }; }; } 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.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); } return false; }; // IE, Edge if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { TextDecoder = TextDecoderPolyfill; } var k = function () { }; function XHRWrapper(xhr) { this.withCredentials = false; this.readyState = 0; this.status = 0; this.statusText = ""; this.responseText = ""; this.onprogress = k; this.onload = k; this.onerror = 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.onabort(null); 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 (type, event) { if (event == null || event.preventDefault == null) { event = { preventDefault: k }; } // 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; if (type === "load") { that.onload(event); } else if (type === "error") { that.onerror(event); } else if (type === "abort") { that.onabort(event); } else { throw new TypeError(); } that.onreadystatechange(); } }; var onReadyStateChange = function (event) { if (xhr != undefined) { // Opera 12 if (xhr.readyState === 4) { if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { onFinish(xhr.responseText === "" ? "error" : "load", event); } } else if (xhr.readyState === 3) { if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 // and in Firefox 3.6 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 if ("onload" in xhr) { xhr.onload = function (event) { onFinish("load", event); }; } if ("onerror" in xhr) { xhr.onerror = function (event) { onFinish("error", event); }; } // 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 if ("onabort" in xhr) { xhr.onabort = function (event) { onFinish("abort", event); }; } if ("onprogress" in xhr) { 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 (significant lag in IE 11) if ("onreadystatechange" in xhr) { xhr.onreadystatechange = function (event) { onReadyStateChange(event); }; } if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; } xhr.open(method, url, true); if ("readyState" in xhr) { // workaround for Opera 12 issue with "progress" events // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) 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 () { // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; }; XHRWrapper.prototype.send = function () { // loading indicator in Safari < ? (6), Chrome < 14, Firefox // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" 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 ?) if ("withCredentials" in xhr) { xhr.withCredentials = this.withCredentials; } 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)]; }; if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 XMLHttpRequest.HEADERS_RECEIVED = 2; } 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.onerror = function (event) { event.preventDefault(); onFinishCallback(new Error("NetworkError")); }; xhr.onload = function () { onFinishCallback(null); }; xhr.onabort = function () { onFinishCallback(null); }; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { 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)); } }; xhr.withCredentials = withCredentials; for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { xhr.setRequestHeader(name, headers[name]); } } xhr.send(); return xhr; }; 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 reader = null; var controller = new AbortController(); var signal = controller.signal; var textDecoder = new TextDecoder(); fetch(url, { headers: headers, credentials: withCredentials ? "include" : "same-origin", signal: signal, cache: "no-store" }).then(function (response) { reader = response.body.getReader(); onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); // see https://github.com/promises-aplus/promises-spec/issues/179 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(); }); })["catch"](function (error) { if (error.name === "AbortError") { return undefined; } else { return error; } }).then(function (error) { onFinishCallback(error); }); return { abort: function () { if (reader != null) { reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 } controller.abort(); } }; }; 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); function ErrorEvent(type, options) { Event.call(this, type); this.error = options.error; } ErrorEvent.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(;.*)?$/i; var MINIMUM_DURATION = 1000; var MAXIMUM_DURATION = 18000000; var parseDuration = function (value, def) { var n = value == null ? def : 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); options = options || {}; this.onopen = undefined; this.onmessage = undefined; this.onerror = undefined; this.url = undefined; this.readyState = undefined; this.withCredentials = undefined; this.headers = undefined; this._close = undefined; start(this, url, options); } function getBestXHRTransport() { return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined ? new XMLHttpRequest() : new XDomainRequest(); } var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; function start(es, url, options) { url = String(url); var withCredentials = Boolean(options.withCredentials); var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; var initialRetry = clampDuration(1000); var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); var lastEventId = ""; var retry = initialRetry; var wasActivity = false; var textLength = 0; var headers = options.headers || {}; var TransportOption = options.Transport; var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); var abortController = 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) { if (currentState === CONNECTING) { if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { currentState = OPEN; wasActivity = Date.now(); 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."; } close(); var event = new ConnectionEvent("error", { status: status, statusText: statusText, headers: headers }); es.dispatchEvent(event); fire(es, es.onerror, event); console.error(message); } } }; 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 (textChunk !== "") { wasActivity = Date.now(); textLength += textChunk.length; } 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 === "open") { fire(es, es.onopen, event); } else if (eventTypeBuffer === "message") { fire(es, es.onmessage, event); } else if (eventTypeBuffer === "error") { fire(es, es.onerror, 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 (error) { 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 ErrorEvent("error", {error: error}); es.dispatchEvent(event); fire(es, es.onerror, event); if (error != undefined) { console.error(error); } } }; var close = function () { currentState = CLOSED; if (abortController != undefined) { abortController.abort(); abortController = undefined; } if (timeout !== 0) { clearTimeout(timeout); timeout = 0; } es.readyState = CLOSED; }; var onTimeout = function () { timeout = 0; if (currentState !== WAITING) { if (!wasActivity && abortController != undefined) { onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); if (abortController != undefined) { abortController.abort(); abortController = undefined; } } else { var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); wasActivity = false; timeout = setTimeout(function () { onTimeout(); }, nextHeartbeat); } return; } wasActivity = false; textLength = 0; 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 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); } } var withCredentials = es.withCredentials; var requestHeaders = {}; requestHeaders["Accept"] = "text/event-stream"; var headers = es.headers; if (headers != undefined) { for (var name in headers) { if (Object.prototype.hasOwnProperty.call(headers, name)) { requestHeaders[name] = headers[name]; } } } try { abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); } catch (error) { close(); throw error; } }; es.url = url; es.readyState = CONNECTING; es.withCredentials = withCredentials; es.headers = headers; 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; var R = NativeEventSource; if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { // 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 // ... R = EventSourcePolyfill; } (function (factory) { { var v = factory(exports); if (v !== undefined) { module.exports = v; } } })(function (exports) { exports.EventSourcePolyfill = EventSourcePolyfill; exports.NativeEventSource = NativeEventSource; exports.EventSource = R; }); }(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal) : globalThis)); }); var eventsource$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.assign(/*#__PURE__*/Object.create(null), eventsource, { 'default': eventsource })); var formatText = function (e) { return e.data; }; var formatJSON = function (e) { return JSON.parse(e.data); }; var SSEClient = function SSEClient(config) { this._handlers = {}; this._listeners = {}; this._source = null; if (config.format) { if (typeof config.format === 'string') { if (config.format === 'plain') { this._format = formatText; } else if (config.format === 'json') { this._format = formatJSON; } else { this._format = formatText; } } else if (typeof config.format === 'function') { this._format = config.format; } else { this._format = formatText; } } else { this._format = formatText; } if (config.handlers) { for (var event in config.handlers) { this.on(event, config.handlers[event]); } } this.url = config.url; this.withCredentials = !!config.withCredentials; this.polyfillOptions = config.polyfillOptions || {}; this.forcePolyfill = !!config.forcePolyfill; }; var prototypeAccessors = { source: { configurable: true } }; prototypeAccessors.source.get = function () { return this._source; }; SSEClient.prototype.connect = function connect () { var this$1 = this; if (this.forcePolyfill) { this._source = new eventsource.EventSourcePolyfill( this.url, Object.assign({}, this.polyfillOptions, { withCredentials: this.withCredentials, }) ); } else { this._source = new window.EventSource(this.url, { withCredentials: this.withCredentials, }); } return new Promise(function (resolve, reject) { this$1._source.onopen = function () { // Add event listeners that were added before we connected for (var event in this$1._listeners) { this$1._source.addEventListener(event, this$1._listeners[event]); } this$1._source.onerror = null; resolve(this$1); }; this$1._source.onerror = reject; }); }; SSEClient.prototype.disconnect = function disconnect () { if (this._source !== null) { this._source.close(); this._source = null; } }; SSEClient.prototype.on = function on (event, handler) { if (!event) { // Default "event-less" event event = 'message'; } if (!this._listeners[event]) { this._create(event); } this._handlers[event].push(handler); return this; }; SSEClient.prototype.once = function once (event, handler) { var this$1 = this; this.on(event, function (e) { this$1.off(event, handler); handler(e); }); return this; }; SSEClient.prototype.off = function off (event, handler) { if (!this._handlers[event]) { // no handlers registered for event return this; } var idx = this._handlers[event].indexOf(handler); if (idx === -1) { // handler not registered for event return this; } // remove handler from event this._handlers[event].splice(idx, 1); if (this._handlers[event].length === 0) { // remove listener since no handlers exist this._source.removeEventListener(event, this._listeners[event]); delete this._handlers[event]; delete this._listeners[event]; } return this; }; SSEClient.prototype._create = function _create (event) { var this$1 = this; this._handlers[event] = []; this._listeners[event] = function (message) { var data; try { data = this$1._format(message); } catch (err) { if (typeof this$1._source.onerror === 'function') { this$1._source.onerror(err); } return; } this$1._handlers[event].forEach(function (handler) { return handler(data, message.lastEventId); }); }; if (this._source) { this._source.addEventListener(event, this._listeners[event]); } }; Object.defineProperties( SSEClient.prototype, prototypeAccessors ); function install(Vue, config) { if (Vue.config && Vue.config.globalProperties) { // Vue3 Vue.config.globalProperties.$sse = new SSEManager(config); } else { // Vue2 // eslint-disable-next-line no-param-reassign, no-multi-assign Vue.$sse = Vue.prototype.$sse = new SSEManager(config); } if (config && config.polyfill) { Promise.resolve().then(function () { return eventsource$1; }); } // This mixin allows components to specify that all clients that were // created within it should be automatically disconnected (cleanup) // when the component is destroyed. Vue.mixin({ beforeCreate: function beforeCreate() { if (this.$options.sse && this.$options.sse.cleanup) { // We instantiate an SSEManager for this specific instance // in order to track it (see discussions in #13 for rationale). this.$sse = new SSEManager(); // We also set $clients to an empty array, as opposed to null, // so that beforeDestroy and create know to use it. this.$sse.$clients = []; } }, beforeDestroy: function beforeDestroy() { if (this.$sse.$clients !== null) { this.$sse.$clients.forEach(function (c) { return c.disconnect(); }); this.$sse.$clients = []; } }, }); } var SSEManager = function SSEManager(config) { this.$defaultConfig = Object.assign( { format: formatText, sendCredentials: false, }, config ); this.$clients = null; }; SSEManager.prototype.create = function create (configOrURL) { var config; if (typeof configOrURL === 'object') { config = configOrURL; } else if (typeof configOrURL === 'string') { config = { url: configOrURL, }; } else { config = {}; } var client = new SSEClient(Object.assign({}, this.$defaultConfig, config)); // If $clients is not null, then it's array that we should push this // client into for later cleanup in our mixin's beforeDestroy. if (this.$clients !== null) { this.$clients.push(client); } return client; }; var index_cjs = { SSEManager: SSEManager, install: install, }; return index_cjs; }))); ================================================ FILE: dist/vue-sse.mjs ================================================ import VueSSE from './vue-sse.common.js'; const { SSEManager, install, } = VueSSE; export { VueSSE as default, SSEManager, install, }; ================================================ FILE: jest.config.js ================================================ module.exports = { collectCoverageFrom: [ 'src/**.*.js', '!src/index.cjs.js', ], coverageDirectory: 'coverage', coverageProvider: 'v8', rootDir: __dirname, setupFilesAfterEnv: ['./test/setup.js'], testMatch: ['/test/**/*.spec.js'], testPathIgnorePatterns: ['/node_modules/'], transform: { '^.+\\.js$': '/node_modules/babel-jest', }, }; ================================================ FILE: package.json ================================================ { "name": "vue-sse", "version": "2.5.2", "description": "A Vue plugin for using Server-Sent Events (EventSource)", "main": "dist/vue-sse.common.js", "exports": { ".": { "module": "./dist/vue-sse.esm.js", "require": "./dist/vue-sse.common.js", "import": "./dist/vue-sse.mjs" }, "./": "./" }, "module": "dist/vue-sse.esm.js", "typings": "types/index.d.ts", "unpkg": "dist/vue-sse.js", "jsdelivr": "dist/vue-sse.js", "sideEffects": [ "./src/sse-client.js" ], "files": [ "dist", "types/*.d.ts" ], "scripts": { "build": "rollup -c rollup.config.js", "format": "prettier --write \"src/**/*.js\"", "lint": "eslint src test", "postversion": "git push && git push --tags", "prepare": "npm run build", "prepublishOnly": "npm test && npm run lint", "preversion": "npm run lint", "test": "npm run lint && npm run test:unit", "test:unit": "jest", "test:types": "tsc -p types/test", "version": "npm run format && git add -A src" }, "repository": { "type": "git", "url": "git+https://github.com/tserkov/vue-sse.git" }, "keywords": [ "vue", "sse", "eventsource", "server sent events" ], "author": "tserkov", "license": "MIT", "bugs": { "url": "https://github.com/tserkov/vue-sse/issues" }, "homepage": "https://github.com/tserkov/vue-sse", "dependencies": { "event-source-polyfill": "^1.0.22" }, "peerDependencies": { "vue": "^2.0.0 || ^3.0.0" }, "devDependencies": { "@babel/core": "^7.12.17", "@babel/preset-env": "^7.12.17", "@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-node-resolve": "^11.2.0", "@vue/test-utils": "^1.1.4", "babel-jest": "^26.6.3", "babel-loader": "^8.2.2", "eslint": "^7.20.0", "eslint-plugin-vue": "^7.6.0", "jest": "^26.6.3", "mocksse": "^1.0.2", "rollup": "^2.37.1", "rollup-plugin-terser": "^7.0.2", "typescript": "^4.1.5", "vue": "^2.6.12", "vue-loader": "^15.9.6" } } ================================================ FILE: rollup.config.js ================================================ import buble from '@rollup/plugin-buble'; import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import { terser } from 'rollup-plugin-terser' import pkg from './package.json'; const banner = `/*! * vue-sse v${pkg.version} * (c) ${new Date().getFullYear()} James Churchard * @license MIT */` export default [ { input: 'src/index.js', output: { banner, file: 'dist/vue-sse.esm.browser.js', format: 'es', }, plugins: [ resolve(), commonjs(), ], inlineDynamicImports: true, }, { input: 'src/index.js', output: { banner, file: 'dist/vue-sse.esm.browser.min.js', format: 'es', }, plugins: [ resolve(), commonjs(), terser({ module: true }) ], inlineDynamicImports: true, }, { input: 'src/index.js', output: { banner, file: 'dist/vue-sse.esm.js', format: 'es', }, plugins: [ buble(), resolve(), commonjs(), ], inlineDynamicImports: true, }, { input: 'src/index.cjs.js', output: { banner, file: 'dist/vue-sse.js', format: 'umd', name: 'VueSSE', }, plugins: [ buble(), resolve(), commonjs(), ], inlineDynamicImports: true, }, { input: 'src/index.cjs.js', output: { banner, file: 'dist/vue-sse.min.js', format: 'umd', name: 'VueSSE', }, plugins: [ buble(), resolve(), commonjs(), terser({ module: false }) ], inlineDynamicImports: true, }, { input: 'src/index.cjs.js', output: { banner, file: 'dist/vue-sse.common.js', format: 'cjs', exports: 'default', }, plugins: [ buble(), resolve(), commonjs(), ], inlineDynamicImports: true, }, ]; ================================================ FILE: src/index.cjs.js ================================================ import SSEManager, { install } from './sse-manager'; export default { SSEManager, install, }; ================================================ FILE: src/index.js ================================================ import SSEManager, { install } from './sse-manager'; export default { install, }; export { SSEManager, install }; ================================================ FILE: src/sse-client.js ================================================ import { EventSourcePolyfill } from 'event-source-polyfill'; export const formatText = (e) => e.data; export const formatJSON = (e) => JSON.parse(e.data); export default class SSEClient { constructor(config) { this._handlers = {}; this._listeners = {}; this._source = null; if (config.format) { if (typeof config.format === 'string') { if (config.format === 'plain') { this._format = formatText; } else if (config.format === 'json') { this._format = formatJSON; } else { this._format = formatText; } } else if (typeof config.format === 'function') { this._format = config.format; } else { this._format = formatText; } } else { this._format = formatText; } if (config.handlers) { for (const event in config.handlers) { this.on(event, config.handlers[event]); } } this.url = config.url; this.withCredentials = !!config.withCredentials; this.polyfillOptions = config.polyfillOptions || {}; this.forcePolyfill = !!config.forcePolyfill; } get source() { return this._source; } connect() { if (this.forcePolyfill) { this._source = new EventSourcePolyfill( this.url, Object.assign({}, this.polyfillOptions, { withCredentials: this.withCredentials, }), ); } else { this._source = new window.EventSource(this.url, { withCredentials: this.withCredentials, }); } return new Promise((resolve, reject) => { this._source.onopen = () => { // Add event listeners that were added before we connected for (let event in this._listeners) { this._source.addEventListener(event, this._listeners[event]); } this._source.onerror = null; resolve(this); }; this._source.onerror = reject; }); } disconnect() { if (this._source !== null) { this._source.close(); this._source = null; } } on(event, handler) { if (!event) { // Default "event-less" event event = 'message'; } if (!this._listeners[event]) { this._create(event); } this._handlers[event].push(handler); return this; } once(event, handler) { this.on(event, (e) => { this.off(event, handler); handler(e); }); return this; } off(event, handler) { if (!this._handlers[event]) { // no handlers registered for event return this; } const idx = this._handlers[event].indexOf(handler); if (idx === -1) { // handler not registered for event return this; } // remove handler from event this._handlers[event].splice(idx, 1); if (this._handlers[event].length === 0) { // remove listener since no handlers exist this._source.removeEventListener(event, this._listeners[event]); delete this._handlers[event]; delete this._listeners[event]; } return this; } _create(event) { this._handlers[event] = []; this._listeners[event] = (message) => { let data; try { data = this._format(message); } catch (err) { if (typeof this._source.onerror === 'function') { this._source.onerror(err); } return; } this._handlers[event].forEach((handler) => handler(data, message.lastEventId)); }; if (this._source) { this._source.addEventListener(event, this._listeners[event]); } } } ================================================ FILE: src/sse-manager.js ================================================ import SSEClient, { formatText } from './sse-client'; export function install(Vue, config) { if (Vue.config && Vue.config.globalProperties) { // Vue3 Vue.config.globalProperties.$sse = new SSEManager(config); } else { // Vue2 // eslint-disable-next-line no-param-reassign, no-multi-assign Vue.$sse = Vue.prototype.$sse = new SSEManager(config); } if (config && config.polyfill) { import('event-source-polyfill'); } // This mixin allows components to specify that all clients that were // created within it should be automatically disconnected (cleanup) // when the component is destroyed. Vue.mixin({ beforeCreate() { if (this.$options.sse && this.$options.sse.cleanup) { // We instantiate an SSEManager for this specific instance // in order to track it (see discussions in #13 for rationale). this.$sse = new SSEManager(); // We also set $clients to an empty array, as opposed to null, // so that beforeDestroy and create know to use it. this.$sse.$clients = []; } }, beforeDestroy() { if (this.$sse.$clients !== null) { this.$sse.$clients.forEach((c) => c.disconnect()); this.$sse.$clients = []; } }, }); } export class SSEManager { constructor(config) { this.$defaultConfig = Object.assign( { format: formatText, sendCredentials: false, }, config, ); this.$clients = null; } create(configOrURL) { let config; if (typeof configOrURL === 'object') { config = configOrURL; } else if (typeof configOrURL === 'string') { config = { url: configOrURL, }; } else { config = {}; } const client = new SSEClient(Object.assign({}, this.$defaultConfig, config)); // If $clients is not null, then it's array that we should push this // client into for later cleanup in our mixin's beforeDestroy. if (this.$clients !== null) { this.$clients.push(client); } return client; } } export default SSEManager; ================================================ FILE: test/.eslintrc.json ================================================ { "env": { "jest": true } } ================================================ FILE: test/setup.js ================================================ import { EventSource } from 'mocksse'; Object.defineProperty(global, 'window', { value: { EventSource, navigator: { userAgent: 'jest', }, }, }); ================================================ FILE: test/sse-client.spec.js ================================================ import { MockEvent } from 'mocksse'; import SSEClient from '../src/sse-client'; describe('SSEClient', () => { it('can receive eventless text messages', (done) => { new MockEvent({ url: '/eventless-text', responses: [{ type: 'message', data: 'a short message', }], }); const client = new SSEClient({ url: '/eventless-text', format: 'plain', handlers: { message: (msg) => { try { expect(msg).toEqual('a short message'); done(); } catch (err) { done(err); } finally { client.disconnect(); } }, }, }); client.connect(); }); it('can receive eventless json messages', (done) => { new MockEvent({ url: '/eventless-json', responses: [{ type: 'message', data: '{"pi":3.14}', }], }); const client = new SSEClient({ url: '/eventless-json', format: 'json', handlers: { message: (msg) => { try { expect(msg).toStrictEqual({ pi: 3.14 }); done(); } catch (err) { done(err); } finally { client.disconnect(); } }, }, }); client.connect(); }); it('can receive custom event messages', (done) => { new MockEvent({ url: '/custom-event', responses: [{ type: 'ping', data: 'ok!', }], }); const client = new SSEClient({ url: '/custom-event', handlers: { ping: (msg) => { try { expect(msg).toStrictEqual('ok!'); done(); } catch (err) { done(err); } finally { client.disconnect(); } }, }, }); client.connect(); }); }); ================================================ FILE: test/sse-manager.spec.js ================================================ import { createLocalVue, mount } from '@vue/test-utils'; import SSEManager from '../src/sse-manager'; import VueSSE from '../src/index'; describe('SSEManager', () => { it('creates a client that inherits from manager config', () => { const $sse = new SSEManager({ url: 'foo.local', withCredentials: true, }); const client = $sse.create({}); expect(client.url).toEqual('foo.local'); expect(client.withCredentials).toEqual(true); }); it('creates a client that overrides manager config', () => { const $sse = new SSEManager({ url: 'foo.local', withCredentials: true, }); const client = $sse.create({ url: 'bar.local', withCredentials: false, }); expect(client.url).toEqual('bar.local'); expect(client.withCredentials).toEqual(false); }); it('creates a client from url string not config object', () => { const $sse = new SSEManager(); const client = $sse.create('foo.local'); expect(client.url).toEqual('foo.local'); }); it('creates a client and cleans up', () => { const localVue = createLocalVue() localVue.use(VueSSE); const wrapper = mount({ template: '
', sse: { cleanup: true, }, mounted() { this.$sse.create('bar.local'); }, }, { localVue }); wrapper.destroy(); expect(wrapper.vm.$sse.$clients).not.toBe(null); expect(wrapper.vm.$sse.$clients).toHaveLength(0); }); it('creates a client and does not clean up', () => { const localVue = createLocalVue() localVue.use(VueSSE); const wrapper = mount({ template: '
', sse: { cleanup: false, }, mounted() { this.$sse.create('bar.local'); }, }, { localVue }); wrapper.destroy(); expect(wrapper.vm.$sse.$clients).toBe(null); }); }); ================================================ FILE: types/index.d.ts ================================================ import './vue'; import Vue from 'vue'; export type MessageFormatter = (event: MessageEvent) => T; export type MessageHandler = (data: any, lastEventId: string) => void; export interface SSEConfig { format?: 'plain' | 'json' | MessageFormatter; handlers?: Partial>; polyfill?: boolean; forcePolyfill?: boolean; polyfillOptions?: object; url?: string; withCredentials?: boolean; } export interface SSEComponentOptions { cleanup?: boolean; } export declare class SSEManager { $defaultConfig: SSEConfig; $clients: SSEClient[] | null; constructor(config?: SSEConfig); create(configOrURL?: SSEConfig | string): SSEClient; } export declare class SSEClient { url: string; withCredentials: boolean; _format: MessageFormatter; _handlers: Partial>; _listeners: Partial>; _source: EventSource; constructor(config?: SSEConfig); connect(): Promise; disconnect(): void; on(event: string, handler: MessageHandler): SSEClient; once(event: string, handler: MessageHandler): SSEClient; off(event: string, handler: MessageHandler): SSEClient; get source(): EventSource; } export declare function install(vue: typeof Vue, config?: SSEConfig): void; declare const _default: { SSEClient: typeof SSEClient; SSEManager: typeof SSEManager; install: typeof install; } export default _default; ================================================ FILE: types/vue.d.ts ================================================ import Vue from 'vue'; import { SSEComponentOptions, SSEManager } from './index'; declare module 'vue/types/vue' { interface VueConstructor { readonly $sse: SSEManager; } interface Vue { $sse: SSEManager; } } declare module 'vue/types/options' { interface ComponentOptions { sse?: SSEComponentOptions; } }