UNPKG

@openshift-console/dynamic-plugin-sdk

Version:

Provides core APIs, types and utilities used by dynamic plugins at runtime.

255 lines (254 loc) 7.64 kB
const createURL = (host, path) => { let url; if (host === 'auto') { if (window.location.protocol === 'https:') { url = 'wss://'; } else { url = 'ws://'; } url += window.location.host; } else { url = host; } if (path) { url += path; } return url; }; /** * @class WebSocket factory and utility wrapper. */ export class WSFactory { /** * @param id - unique id for the WebSocket. * @param options - websocket options to initate the WebSocket with. */ constructor(id, options) { this.id = id; this.options = options; this.bufferMax = options.bufferMax || 0; this.paused = false; this.handlers = { open: [], close: [], error: [], message: [], destroy: [], bulkmessage: [], }; this.connect(); if (this.bufferMax) { this.flushCanceler = setInterval(this.flushMessageBuffer.bind(this), this.options.bufferFlushInterval || 500); } } reconnect() { if (this.connectionAttempt || this.state === 'destroyed') { return; } let delay = 1000; const attempt = () => { if (!this.options.reconnect || this.state === 'open') { clearTimeout(this.connectionAttempt); this.connectionAttempt = null; return; } if (this.options.timeout && delay > this.options.timeout) { clearTimeout(this.connectionAttempt); this.connectionAttempt = null; this.destroy(); return; } this.connect(); delay = Math.round(Math.min(1.5 * delay, 60000)); this.connectionAttempt = setTimeout(attempt, delay); // eslint-disable-next-line no-console console.log(`attempting reconnect in ${delay / 1000} seconds...`); }; this.connectionAttempt = setTimeout(attempt, delay); } connect() { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; this.state = 'init'; this.messageBuffer = []; const url = createURL(this.options.host, this.options.path); try { this.ws = new WebSocket(url, this.options.subprotocols); } catch (e) { // eslint-disable-next-line no-console console.error('Error creating websocket:', e); this.reconnect(); return; } this.ws.onopen = function () { // eslint-disable-next-line no-console console.log(`websocket open: ${that.id}`); that.state = 'open'; that.triggerEvent('open', undefined); if (that.connectionAttempt) { clearTimeout(that.connectionAttempt); that.connectionAttempt = null; } }; this.ws.onclose = function (evt) { // eslint-disable-next-line no-console console.log(`websocket closed: ${that.id}`, evt); that.state = 'closed'; that.triggerEvent('close', evt); that.reconnect(); }; this.ws.onerror = function (evt) { // eslint-disable-next-line no-console console.log(`websocket error: ${that.id}`); that.state = 'error'; that.triggerEvent('error', evt); }; this.ws.onmessage = function (evt) { const msg = that.options?.jsonParse ? JSON.parse(evt.data) : evt.data; // In some browsers, onmessage can fire after onclose/error. Don't update state to be incorrect. if (that.state !== 'destroyed' && that.state !== 'closed') { that.state = 'open'; } that.triggerEvent('message', msg); }; } registerHandler(type, fn) { if (this.state === 'destroyed') { return; } this.handlers[type].push(fn); } /** * Invoke all registered handler callbacks for a given event type. */ invokeHandlers(type, data) { const handlers = this.handlers[type]; if (!handlers) { return; } handlers.forEach(function (h) { try { h(data); } catch (e) { // eslint-disable-next-line no-console console.error('WS handling failed:', e); } }); } /** * Triggers event to be buffered or invoked depending on config. */ triggerEvent(type, event) { if (this.state === 'destroyed') { return; } // Only buffer "message" events, so "error" and "close" etc can pass thru. if (this.bufferMax && type === 'message') { this.messageBuffer.push(event); if (this.messageBuffer.length > this.bufferMax) { this.messageBuffer.shift(); } return; } this.invokeHandlers(type, event); } onmessage(fn) { this.registerHandler('message', fn); return this; } onbulkmessage(fn) { this.registerHandler('bulkmessage', fn); return this; } onerror(fn) { this.registerHandler('error', fn); return this; } onopen(fn) { this.registerHandler('open', fn); return this; } onclose(fn) { this.registerHandler('close', fn); return this; } ondestroy(fn) { this.registerHandler('destroy', fn); return this; } flushMessageBuffer() { if (this.paused) { return; } if (!this.messageBuffer.length) { return; } if (this.handlers.bulkmessage.length) { this.invokeHandlers('bulkmessage', this.messageBuffer); } else { this.messageBuffer.forEach((e) => this.invokeHandlers('message', e)); } this.messageBuffer = []; } /** * Pausing prevents any buffer flushing until unpaused. */ pause() { this.paused = true; } unpause() { this.paused = false; this.flushMessageBuffer(); } isPaused() { return this.paused; } getState() { return this.state; } bufferSize() { return this.messageBuffer.length; } destroy(eventData) { // eslint-disable-next-line no-console console.log(`destroying websocket: ${this.id}`); if (this.state === 'destroyed') { return; } try { if (this.ws) { this.ws.close(); } } catch (e) { // eslint-disable-next-line no-console console.error('Error while close WS socket', e); } clearInterval(this.flushCanceler); clearTimeout(this.connectionAttempt); if (this.ws) { this.ws.onopen = null; this.ws.onclose = null; this.ws.onerror = null; this.ws.onmessage = null; delete this.ws; } try { this.triggerEvent('destroy', eventData); } catch (e) { // eslint-disable-next-line no-console console.error('Error while trigger destroy event for WS socket', e); } this.state = 'destroyed'; this.messageBuffer = []; } send(data) { this.ws && this.ws.send(data); } }