class WSElement extends HTMLElement { static #sockets = {}; #socket; #delay = 1000; constructor() { super(); } connectedCallback() { // track `src` and `autoreconnect` attributes for later use // `autoreconnect` defaults to `true`, unless explicitly set to `false` this.src = this.getAttribute('src'); this.autoreconnect = !this.hasAttribute('autoreconnect') || this.getAttribute('autoreconnect').toLowerCase() !== 'false'; this.#connect_socket(false); } #connect_socket(recreate) { // `recreate` is used to handle reconnects, removeing the old websocket object // and recreating a new one if(recreate && WSElement.#sockets[this.src]) { delete WSElement.#sockets[this.src]; } // a single WebSocket can be reused for multiple elements // check to see if one exists for this `src` else create // a new one. if(!(this.src in WSElement.#sockets)) { this.#socket = WSElement.#newSocket(this.src); WSElement.#sockets[this.src] = this.#socket; } else { this.#socket = WSElement.#sockets[this.src]; } // setup event handlers for this WebSocket/Elemnent pair this.#socket.addEventListener('error', this.#on_ws_error.bind(this)); this.#socket.addEventListener('message', this.#on_ws_message.bind(this)); this.#socket.addEventListener('open', this.#on_ws_open.bind(this)); this.#socket.addEventListener('close', this.#on_ws_close.bind(this)); } #on_ws_open(sock) { // set connected attribute for use in CSS // ``` // ws-element[connected] { // color: green; // } // ws-element:not(connected) { // color: red; // } // ``` this.setAttribute('connected', ''); // call this.open if defined by subclass if('open' in this) { this.open(sock); } } #on_ws_message(sock) { // call this.message, which is always defined this.message(sock.data); } #on_ws_close(sock) { this.removeAttribute('connected'); // clean up event handlers for this element sock.target.removeEventListener('error', this.#on_ws_error.bind(this)); sock.target.removeEventListener('message', this.#on_ws_message.bind(this)); sock.target.removeEventListener('open', this.#on_ws_open.bind(this)); sock.target.removeEventListener('close', this.#on_ws_close.bind(this)); // call this.close if subclass has defined it if('close' in this) { this.close(sock); } // handle autoreconnect and delay throttling if(this.autoreconnect) { setTimeout(this.#connect_socket.call(this, true), this.#delay); if(this.#delay < 10000) { this.#delay += 1000; } } } #on_ws_error(sock) { // call this.error if subclass has defined it if('error' in this) { this.error(sock); } } // default behavior on this.message is just update innerText message(data) { this.innerText = data; } // static method for creating new sockets, which are tracked // on the static propert WSElement.#sockets static #newSocket(src) { var s = window.location.protocol.slice(-2) === 'p:' ? '' : 's'; var host = window.location.host; var path = src[0] === '/' ? src.slice(1) : src; var wsurl = `ws${s}://${host}/${path}`; return new WebSocket(wsurl); } } // Example subclass to display a basic 'Time: 00:00:00' class WSTime extends WSElement { connectedCallback() { super.connectedCallback(); // insert subelements for the label (Time:) and the value; this.insertAdjacentHTML('beforeend', ''); // track the value element for updating later this.value = this.querySelector('span.time-value'); } // override this.message to update the innerText of the value span element message(data) { this.value.innerText = data; } } // Example subclass to parse arbitrary json and build a table // with dynamically controlled limit class WSTable extends WSElement { // observe limit to dynamically update static observedAttributes = [ 'limit' ]; connectedCallback() { super.connectedCallback(); // ignoreNext is to handle duplicate headers being sent on reconnect this.ignoreNext = false; // insert and track table element this.insertAdjacentHTML('beforeend', '