// Websocket is a native global for vanilla JS // /* globals WebSocket:true */ import btc from 'better-try-catch' import EventEmitter from 'eventemitter3' import autoBind from 'auto-bind' import WS from 'reconnecting-websocket' /** * Web Socket Consumer - An in browser consumer/client that can communicate via UCI packets * extends {@link https://github.com/primus/eventemitter3 event emitter 3} an in browser event emitter * uses the browser built in vanilla js global {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket Websocket client class} * @extends EventEmitter */ class WSConsumer extends EventEmitter { /** * constructor - Description * * @param {type} url URL of UCI websocket server * */ constructor (url, opts = {}) { super() this.name = opts.name || 'browser' this.instanceID = new Date().getTime() this.url = url this.wsopts = opts.ws || { maxReconnectionDelay: 10000,minReconnectionDelay: 1000 + Math.random() * 4000,reconnectionDelayGrowFactor: 1.3,minUptime: 5000,connectionTimeout: 4000,maxRetries: Infinity,debug: false,} this.protocol = opts.protocol // available if needed but not documented this.socket = {} autoBind(this) } /** * connect - After instantiating this must be called to connect to a UCI WesbSocket Sever * @required * as coded will reconnect and set up listeners again if the socket gets closed * commented code work * but opted to use https://www.npmjs.com/package/reconnecting-websocket */ // async connect () { // return new Promise((resolve, reject) => { // const init = (socket) => { // if (socket) console.error('Disconnected from Server') // this.socket = new WebSocket(this.url, this.protocol) // this.socket.addEventListener('close', init) // this.socket.addEventListener('open', open.bind(this)) // } // // setTimeout(function () { // reject(new Error('Socket did not initially connect in 20 seconds')) // }, 20000) // // init() // // function open () { // this.listen() // resolve(`socket open to server at : ${this.url}`) // } // }) // } async connect () { return new Promise((resolve, reject) => { this.socket = new WS(this.url, this.protocol, this.wsopts) this.socket.onopen = open.bind(this) setTimeout(function () { reject(new Error('Socket did not initially connect in 20 seconds')) }, 20000) // this.socket.onerror = (ev) => { reject(`could not connect/reconnect to server : ${ev}`)} function open () { this.listen() resolve(`socket open to server at : ${this.url}`) } }) } /** * listen - Description * * @param {type} func Description * * @returns {type} Description */ listen (func) { this.socket.addEventListener('message', handler.bind(this)) this.on('pushed', async function (packet) { // TODO do some extra security here for 'evil' pushed packets let res = await this._packetProcess(packet) if (!res) { // if process was not promise returning like just logged to console console.log( 'warning: consumer process function was not promise returning' ) } }) function handler (event) { let packet = {} if (this.socket.readyState === 1) { let [err, parsed] = btc(JSON.parse)(event.data) if (err) packet = { error: `Could not parse JSON: ${event.data}` } else packet = parsed } else { packet = { error: `Connection not Ready, CODE:${this.socket.readyState}` } } // console.log('in the handler', event.data) if (func) func(packet) this.emit(packet._header.id, packet) } } /** * send - Description * * @param {type} packet Description * * @returns {type} Description */ async send (packet) { return new Promise((resolve, reject) => { if (this.socket.readyState !== 1) reject( new Error(`Connection not Ready, CODE:${this.socket.readyState}`) ) packet._header = { id: Math.random() .toString() .slice(2), // need this for when multiple sends for different consumers use same packet instanceack sender: { name: this.name, instanceID: this.instanceID }, url: this.url } let [err, message] = btc(JSON.stringify)(packet) if (err) reject(new Error(`Could not JSON stringify: ${packet}`)) // console.log('message to send', message) this.socket.send(message) // listen for when packet comes back with unique header id this.once(packet._header.id, async function (reply) { let res = await this._packetProcess(reply) if (!res) { // if process was not promise returning like just logged to console res = reply console.log( 'consumer function was not promise returning - resolving unprocessed' ) } resolve(res) }) // end reply listener }) } /** * registerPacketProcessor - attaches the passed packet function as the one to process incoming packets * the funcion must take a uci packet object and return a promise whose resolution should be a uci packet if further communication is desir * * @param {function} func function to do the incoming packet processing * */ registerPacketProcessor (func) { this._packetProcess = func } async _packetProcess (packet) { return Promise.resolve(packet) } } // end Consumer Class export default WSConsumer