197 lines
6.6 KiB
JavaScript
197 lines
6.6 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'
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
let count = 1
|
|
|
|
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.rsLimit = opts.rsLimit || 5
|
|
this.initTimeout = opts.initTimeout || 30000
|
|
this.rsDelay = opts.rsDelay || 5000 // in large production you'd want a more robust delay calculation
|
|
this.protocol = opts.protocol // available if needed but not documented
|
|
this.started = false
|
|
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 () {
|
|
// console.log('--- initial connect to websocket at', this.url)
|
|
return new Promise((resolve, reject) => {
|
|
if(!this.url) reject('no url provided!')
|
|
let timeout
|
|
let connect = con.bind(this)
|
|
|
|
function con () {
|
|
// console.log(this.started, this.socket)
|
|
if (this.socket) delete this.socket // make sure previous socket is garabage collected
|
|
this.socket = new WebSocket(this.url, this.protocol)
|
|
// console.log('ready after create', this.socket.readyState, this.socket.onopen, this.socket.onclose)
|
|
this.socket.onopen = open.bind(this)
|
|
|
|
timeout = setTimeout(function () {
|
|
if (!this.started && count===1) console.log('original connection connect failed - retrying')
|
|
console.log(`socket has not ${this.started?'re':''}connected in ${this.rsDelay*count/1000} seconds`)
|
|
count += 1
|
|
if (!this.started && this.rsDelay*count > this.initTimeout) {
|
|
let err = `unable to make a connection to websocket server at ${this.url} within ${this.initTimeout/1000}s`
|
|
console.log(err)
|
|
reject({url:this.url, msg:err})
|
|
}
|
|
else connect()
|
|
}.bind(this), this.rsDelay)
|
|
}
|
|
|
|
function open () {
|
|
this.listen() // this handles messages
|
|
console.log(`socket open to server at : ${this.url}`)
|
|
if (!this.started) this.emit('connected')
|
|
else this.emit('reconnected')
|
|
clearTimeout(timeout)
|
|
// this.socket.onerror = error.bind(this)
|
|
this.socket.onclose = close.bind(this)
|
|
console.log(this.socket)
|
|
count = 0
|
|
this.started = true
|
|
resolve({url:this.url, msg:`socket open to server at : ${this.url}`})
|
|
}
|
|
|
|
connect() // get the ball rolling
|
|
|
|
function close () {
|
|
this.socket.onclose = null
|
|
console.error('Socket has closed, attempting reconnect')
|
|
this.removeAllListeners('push')
|
|
this.socket.onmessage = null
|
|
this.emit('disconnected')
|
|
connect()
|
|
}
|
|
}) // end promise
|
|
}
|
|
|
|
/**
|
|
* listen - Description
|
|
*
|
|
* @param {type} func Description
|
|
*
|
|
* @returns {type} Description
|
|
*/
|
|
listen (func) {
|
|
this.socket.onmessage = packetHandler.bind(this)
|
|
|
|
// process 'pushed' packets
|
|
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 packetHandler (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) // extra processing if enabled
|
|
// this is response to a packet send command listener and is processed below
|
|
// will also emit 'pushed' via id which can be listened for in app
|
|
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
|
|
}
|
|
|
|
// do nothing
|
|
async _packetProcess (packet) {
|
|
return Promise.resolve(packet)
|
|
}
|
|
} // end Consumer Class
|
|
|
|
export default WSConsumer
|