uci-socket/src/consumer.mjs

136 lines
4.0 KiB
JavaScript

import { Socket } from 'net'
import btc from 'better-try-catch'
import bunyan from 'bunyan'
// import Stream from 'delimiter-stream'
import JsonStream from './json-stream'
export default class Consumer extends Socket {
constructor (path={}, opts={}) {
super()
// set or tcp socket
if (typeof(path)!=='string') {
opts = path
if (!path.host || !path.port) opts = path
this.host = path.host || '127.0.0.1' // TODO log a warning about host on same machine
this.port = path.port || 8080
} else this.path = path
this.packet = {
_process: async (packet) => {
console.log('default processor -- packet from socket')
console.dir(packet)
return packet }
}
this.keepAlive = opts.keepAlive ? opts.keepAlive : true
this._ready = false
this.timeout = opts.timeout || 500
this.wait = opts.wait || 5
this.stream = new JsonStream()
this.packet = {
_process: (packet) => {
packet.res='echoed'
return packet }
}
// logging
this.log_file=opts.log_file || './socket.log'
this.log_opts = {streams:[]}
this.log_opts.name = opts.name ? opts.name : 'uci-socket-consumer'
// this.log_opts.streams.push({level: 'info',path: this.log_file })
if (opts.log) this.log_opts.streams.push({level: 'info',stream: process.stdout})
this.log = bunyan.createLogger(this.log_opts)
// bind to class for other class functions
this.connect = this.connect.bind(this)
this.ready = this.ready.bind(this)
}
ready() {return this._ready}
async connect () {
// if (context) this.packet.context = context
// else this.packet.context = this
return new Promise( (resolve,reject) => {
const connect = () => {
super.connect({ port:this.port, host:this.host, path: this.path })
}
const timeout = setTimeout(() =>{
reject(`unable to connect in ${this.timeout*10}ms`)
}
,this.timeout*10)
this.once('connect', async () => {
clearTimeout(timeout)
this.listen()
this.log.info({path:this.path},'connected waiting for socket ready handshake')
this.setKeepAlive(this.keepAlive)
let [err, res] = await btc(isReady).bind(this)(this.ready, this.wait, this.timeout)
if (err) reject(err)
this.log.info('handshake done, authenticating')
// TODO authenticate here by encrypting a payload with private key and sending that.
// await btc(authenticate)
resolve(res)
})
this.on('error', async (err) => {
if (err.code === 'EISCONN') {
return resolve('ready')
}
this.log.warn(err.code)
setTimeout(() =>{
this.log.warn('retry connect')
connect()
}
,this.wait*10)
})
connect()
}) //end promise
}
async listen () {
this.log.info('listening for incoming packets from socket')
this.on('data', this.stream.onData)
this.stream.on('message', messageProcess.bind(this))
async function messageProcess (packet) {
if (packet.ready) {
this._ready = true
return }
await this.packet._process(packet)
}
}
async send(packet) {
await this.write(this.stream.serialize(packet))
// TODO handle error here? and/or await response if required before allowing more sending
}
// TODO register alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks)
// TODO register authenciation function (set up default)
registerPacketProcessor (func) {
this.packet._process = func
}
} // end class
// wait for handshake packet from socket
function isReady(ready, wait=30, timeout=1000) {
let log = this.log
let time = 0
return new Promise((resolve, reject) => {
(function waitReady(){
if (time > timeout) return reject(`timeout waiting for socket ready handshake - ${timeout}ms`)
if (ready()) return resolve('ready')
log.info(`waiting ${wait}ms for handshake`)
time += wait
setTimeout(waitReady, wait)
})()
})
}