uci-websocket-client/src/WSConsumer.js

174 lines
5.5 KiB
JavaScript

// 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