import WebSocket from 'ws' import btc from 'better-try-catch' import _ON_DEATH from 'death' //this is intentionally ugly import clone from 'clone' import logger from '@uci/logger' let log = {} export default class Socket extends WebSocket.Server { constructor (opts = {}) { opts.host = opts.host || '0.0.0.0' opts.port = opts.port || 8080 super(opts) this.id = opts.id || opts.name || 'Websocket:'+ new Date().getTime() this.opts = opts // for use to recover from selected errors //self bindings this._listen = this._listen.bind(this) this.create = this.create.bind(this) log = logger.child( { repo:'uci-websocket', npm:'@uci/websocket', file:'src/socket.mjs', class:'Socket', id:this.id, instance_created:new Date().getTime() }) } // end constructor async create () { return new Promise((resolve, reject) => { _ON_DEATH( async () => { log.info('\nhe\'s dead jim') await this._destroy() }) process.once('SIGUSR2', async () => { await this._destroy process.kill(process.pid, 'SIGUSR2') }) this.on('error', async (err) => { log.fatal(err, 'socket server error') console.error(err, 'socket server error') reject(err) }) this.on('listening', async () => { this._listen() log.info('websocket server created and listening at', this.address()) resolve('websocket ready and listening') }) }) } // end create registerPacketProcessor (func) { this._packetProcess = func } _listen () { this.on('connection', async (socket) => { const send = this._send.bind(socket) log.info('new consumer connecting') socket.on('message', messageProcess.bind(this)) async function messageProcess (strPacket) { log.info(' incoming packet on socket side') let res = {} let [err, packet] = btc(JSON.parse)(strPacket) if (err) { res = {error: `Could not parse JSON: ${packet}`} } else { res = await this._packetProcess(clone(packet)) if (Object.keys(res).length === 0) res = { error: 'socket packet command function likely did not return a promise', packet:packet} } res._header = clone(packet._header,false) //make sure return packet has header with id in case it was removed in processing delete packet._header // remove before adding to response header as request res._header.request = clone(packet,false) res._header.responder = {name:this.name,instanceID:this.id} res._header.socket = this.address() if (!res.cmd) res.cmd = 'reply' // by default return command is 'reply' log.info(await send(res)) } }) // end connected consumer log.info('socket created') } // end listen async _destroy () { log.info('closing down socket') await this.close() log.info('all connections closed....exiting') process.exit() } // default packet process, just a simple echo - replace async _packetProcess (packet) { return new Promise(resolve => { resolve(packet) }) } // must have a consumer socket instance bound to call this!! async _send (packet) { return new Promise( (resolve,reject) => { if (this.readyState !== 1 ) reject (`Connection not Ready, CODE:${this.readyState}`) let [err,message] = btc(JSON.stringify)(packet) if (err) reject(`Could not JSON stringify: ${packet}`) this.send(message) resolve('sent packet', packet) }) } } // end class