@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
JavaScript
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);
}
}