Skip to content

Instantly share code, notes, and snippets.

@clshortfuse
Created January 14, 2023 06:15
Show Gist options
  • Save clshortfuse/2120c0e87da8e28c03ae45b127ed5b54 to your computer and use it in GitHub Desktop.
Save clshortfuse/2120c0e87da8e28c03ae45b127ed5b54 to your computer and use it in GitHub Desktop.
EventSource extension with JSON parsing and FireFox workaround
/**
* @implements {EventSource}
*/
export default class JSONEventSource {
#closeCalled;
#url;
#initArgs;
/** @type {EventSource} */
#eventSource;
/** @type {WeakMap<any, {callback: EventListener, options: boolean | AddEventListenerOptions}>} */
#callbackMap = new WeakMap();
/** @type {[string, {callback: EventListener, options: boolean | AddEventListenerOptions}][]} */
#callbackEntries = [];
/**
* @param {string|URL} url
* @param {EventSourceInit} [initArgs]
*/
constructor(url, initArgs) {
this.#url = url;
this.#initArgs = initArgs;
// this.#callbackMap = new WeakMap();
this.#buildEventSource();
this.#closeCalled = false;
}
#buildEventSource() {
this.#closeCalled = false;
this.#eventSource = new EventSource(this.#url, this.#initArgs);
const onErrorCallback = (event) => {
const ev = event.target;
if (ev !== this.#eventSource) return;
switch (event.target?.readyState) {
case EventSource.CLOSED:
console.warn('EventSource closed!');
if (!this.#closeCalled) {
console.warn('Out of spec EventSource implementation. Rebuilding EventSource...');
ev.removeEventListener('error', onErrorCallback);
ev.close();
this.#buildEventSource();
console.warn('EventSource built.');
// Remove all event listeners, wait one event loop cycle to 'error' can bubble
setTimeout(() => {
for (const [type, entry] of this.#callbackEntries) {
ev.removeEventListener(type, entry.callback, entry.options);
}
}, 0);
}
break;
case EventSource.CONNECTING:
// Connection lost
break;
case EventSource.OPEN:
console.warn('EventSource open with error?');
break;
default:
console.log(event.target);
}
};
this.#eventSource.addEventListener('error', onErrorCallback);
this.#eventSource.addEventListener('open', (event) => {
if (event.target === this.#eventSource) {
console.warn('Previous EventSource opened.');
}
});
for (const [type, entry] of this.#callbackEntries) {
this.#eventSource.addEventListener(type, entry.callback, entry.options);
}
}
/**
* @template {string} T
* @template {T extends ('open'|'message'|'error') ? EventListener : (data:Object)=>any} L
* @param {T} type
* @param {L} listener
* @param {boolean | AddEventListenerOptions} [options]
* @return {L}
*/
addEventListener(type, listener, options) {
/**
* @param {MessageEvent} response
* @return {void}
*/
const newCallback = (response) => {
switch (response.type) {
case 'open':
case 'message':
case 'error':
listener(response);
return;
default:
}
const data = JSON.parse(response.data);
listener(data);
};
const entry = { callback: newCallback, options };
this.#callbackMap.set(listener, entry);
this.#callbackEntries.push([type, entry]);
// @ts-ignore Force type
this.#eventSource.addEventListener(type, newCallback, options);
return listener;
}
/**
* @param {Event} event
* @return {boolean}
*/
dispatchEvent(event) {
return this.#eventSource.dispatchEvent(event);
}
/**
* @param {string} type
* @param {EventListener} listener
* @param {boolean | AddEventListenerOptions} [options]
* @return {void}
*/
removeEventListener(type, listener, options) {
const entry = this.#callbackMap.get(listener);
const index = this.#callbackEntries.findIndex(([t, e]) => t === type && e === entry);
if (index !== -1) {
this.#callbackEntries.splice(index, 1);
}
this.#eventSource.removeEventListener(type, entry.callback, options);
}
close() {
this.#closeCalled = true;
this.#eventSource.close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment