0.2.11 refactored consumer connect/reconnect to be more robust
emits both also keeps track of ready statetls
parent
1c3c4383c8
commit
81bb898ab4
|
@ -7,3 +7,4 @@ yarn.lock
|
||||||
travis.yml
|
travis.yml
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
archive/
|
archive/
|
||||||
|
docs/
|
||||||
|
|
|
@ -15,12 +15,11 @@ client.registerPacketProcessor(process)
|
||||||
;
|
;
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// await Promise.all([client1.connect(),client2.connect()])
|
// await Promise.all([client1.connect(),client2.connect()])
|
||||||
await client.connect()
|
await client.connect()
|
||||||
|
console.log('sending packet ', packet)
|
||||||
console.log('=========\n',await client.send(packet))
|
console.log('=========\n',await client.send(packet))
|
||||||
client.end()
|
// client.end()
|
||||||
|
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { Socket as uSocket, sSocket} from '../src'
|
import { Socket as uSocket} from '../src'
|
||||||
import { fs } from 'mz'
|
// import { fs } from 'mz'
|
||||||
|
|
||||||
// made key cert into module that also uses environment variables
|
// made key cert into module that also uses environment variables
|
||||||
const TLS = process.env.TLS || false
|
// const TLS = process.env.TLS || false
|
||||||
const TLS_DIR = process.env.TLS_DIR || '/opt/certs'
|
// const TLS_DIR = process.env.TLS_DIR || '/opt/certs'
|
||||||
const TLS_NAME = process.env.TLD_NAME || 'wc.kebler.net'
|
// const TLS_NAME = process.env.TLD_NAME || 'wc.kebler.net'
|
||||||
const TLS_KEY_PATH = process.env.TLS_KEY_PATH || `${TLS_DIR}/${TLS_NAME}.key`
|
// const TLS_KEY_PATH = process.env.TLS_KEY_PATH || `${TLS_DIR}/${TLS_NAME}.key`
|
||||||
const TLS_CRT_PATH = process.env.TLS_CRT_PATH || `${TLS_DIR}/${TLS_NAME}.crt`
|
// const TLS_CRT_PATH = process.env.TLS_CRT_PATH || `${TLS_DIR}/${TLS_NAME}.crt`
|
||||||
|
|
||||||
let Socket = uSocket
|
let Socket = uSocket
|
||||||
|
|
||||||
;
|
;
|
||||||
(async () => {
|
(async () => {
|
||||||
// TODO dynamic import
|
// TODO dynamic import
|
||||||
if(TLS_KEY_PATH && TLS_CRT_PATH && TLS) {
|
// if(TLS_KEY_PATH && TLS_CRT_PATH && TLS) {
|
||||||
Socket = sSocket
|
// Socket = sSocket
|
||||||
console.log('using TLS')
|
// console.log('using TLS')
|
||||||
}
|
// }
|
||||||
|
|
||||||
class Test extends Socket {
|
class Test extends Socket {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
|
@ -44,22 +44,23 @@ let Socket = uSocket
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
// const options = {
|
||||||
tls: TLS,
|
// tls: TLS,
|
||||||
key: await fs.readFile(TLS_KEY_PATH),
|
// key: await fs.readFile(TLS_KEY_PATH),
|
||||||
cert: await fs.readFile(TLS_CRT_PATH),
|
// cert: await fs.readFile(TLS_CRT_PATH),
|
||||||
// This is necessary only if using client certificate authentication.
|
// // This is necessary only if using client certificate authentication.
|
||||||
// requestCert: true,
|
// // requestCert: true,
|
||||||
// This is necessary only if the client uses a self-signed certificate.
|
// // This is necessary only if the client uses a self-signed certificate.
|
||||||
// ca: [ fs.readFileSync('client-cert.pem') ]
|
// // ca: [ fs.readFileSync('client-cert.pem') ]
|
||||||
}
|
// }
|
||||||
|
|
||||||
options.path = true
|
let options = {path:true}
|
||||||
|
|
||||||
|
|
||||||
// let test = new Test()
|
// let test = new Test()
|
||||||
let test = new Test(options)
|
let test = new Test(options)
|
||||||
await test.create()
|
await test.create()
|
||||||
|
console.log('ready')
|
||||||
|
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@uci/socket",
|
"name": "@uci/socket",
|
||||||
"version": "0.2.10",
|
"version": "0.2.11",
|
||||||
"description": "JSON packet intra(named)/inter(TCP) host communication over socket",
|
"description": "JSON packet intra(named)/inter(TCP) host communication over socket",
|
||||||
"main": "src",
|
"main": "src",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -8,10 +8,10 @@
|
||||||
"test": "mocha -r esm --timeout 10000 test/*.test.mjs",
|
"test": "mocha -r esm --timeout 10000 test/*.test.mjs",
|
||||||
"testlog": "UCI_DEV=true mocha -r esm --timeout 10000 test/*.test.mjs",
|
"testlog": "UCI_DEV=true mocha -r esm --timeout 10000 test/*.test.mjs",
|
||||||
"testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true",
|
"testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true",
|
||||||
"s": "UCI_DEV=true node -r esm examples/server",
|
"s": "UCI_ENV=dev node -r esm examples/server",
|
||||||
"sp": "UCI_DEV=true node -r esm examples/server-push",
|
"sp": "UCI_DEV=true node -r esm examples/server-push",
|
||||||
"devs": "SOCKETS_DIR=/opt/sockets UCI_DEV=true ./node_modules/.bin/nodemon -r esm-e mjs examples/server",
|
"devs": "SOCKETS_DIR=/opt/sockets UCI_DEV=true ./node_modules/.bin/nodemon -r esm-e mjs examples/server",
|
||||||
"c": "UCI_DEV=true node -r esm examples/client",
|
"c": "UCI_ENV=dev node -r esm examples/client",
|
||||||
"cp": "UCI_DEV=true node -r esm examples/client-push",
|
"cp": "UCI_DEV=true node -r esm examples/client-push",
|
||||||
"devc": "SOCKETS_DIR=/opt/sockets UCI_DEV=true node -r esm examples/client",
|
"devc": "SOCKETS_DIR=/opt/sockets UCI_DEV=true node -r esm examples/client",
|
||||||
"c2": "node -r esm examples/client2"
|
"c2": "node -r esm examples/client2"
|
||||||
|
|
144
src/consumer.js
144
src/consumer.js
|
@ -45,9 +45,11 @@ class SocketConsumer extends Socket {
|
||||||
opts.path = path.join(DEFAULT_PIPE_DIR, opts.path)
|
opts.path = path.join(DEFAULT_PIPE_DIR, opts.path)
|
||||||
}
|
}
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
|
// default is keepAlive true, must set to false to explicitly disable
|
||||||
|
// if keepAlive is true then consumer will also be reconnecting consumer
|
||||||
this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true
|
this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true
|
||||||
this._ready = false
|
this._ready = false
|
||||||
this.timeout = opts.timeout || 300 // 5 minutes and then rejects
|
this.timeout = opts.timeout || 60 // initial connect timeout in secs and then rejects
|
||||||
this.wait = opts.wait || 2
|
this.wait = opts.wait || 2
|
||||||
this.stream = new JsonStream()
|
this.stream = new JsonStream()
|
||||||
// bind to class for other class functions
|
// bind to class for other class functions
|
||||||
|
@ -59,22 +61,11 @@ class SocketConsumer extends Socket {
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const connect = () => {
|
|
||||||
if (this.opts.host === '127.0.0.1')
|
|
||||||
log.warn(
|
|
||||||
'tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead'
|
|
||||||
)
|
|
||||||
log.info(
|
|
||||||
{ opts: this.opts },
|
|
||||||
`attempting to connect ${this.id} to socket`
|
|
||||||
)
|
|
||||||
super.connect(this.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
let reconnect = {}
|
let initial = true
|
||||||
|
|
||||||
// this is only for initial connection
|
// this is only for initial connection
|
||||||
const timeout = setTimeout(() => {
|
const initTimeout = setTimeout(() => {
|
||||||
clearTimeout(reconnect)
|
|
||||||
log.fatal({ opts: this.opts }, `unable to connect in ${this.timeout}s`)
|
log.fatal({ opts: this.opts }, `unable to connect in ${this.timeout}s`)
|
||||||
reject(
|
reject(
|
||||||
{ opts: this.opts },
|
{ opts: this.opts },
|
||||||
|
@ -82,47 +73,83 @@ class SocketConsumer extends Socket {
|
||||||
)
|
)
|
||||||
}, this.timeout * 1000)
|
}, this.timeout * 1000)
|
||||||
|
|
||||||
|
|
||||||
this.once('connect', async () => {
|
this.once('connect', async () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(initTimeout)
|
||||||
this._listen()
|
this._listen()
|
||||||
log.info(
|
log.info({ opts: this.opts, msg:'initial connect waiting for socket ready handshake'})
|
||||||
{ opts: this.opts },
|
this.setKeepAlive(this.keepAlive, 3000)
|
||||||
'connected waiting for socket ready handshake'
|
let [err, res] = await btc(isReady).bind(this)(this.__ready,this.wait,this.timeout)
|
||||||
)
|
|
||||||
this.setKeepAlive(this.keepAlive, 100)
|
|
||||||
let [err, res] = await btc(isReady).bind(this)(
|
|
||||||
this.__ready,
|
|
||||||
this.wait,
|
|
||||||
this.timeout
|
|
||||||
)
|
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
log.info('handshake done, authenticating')
|
initial = false
|
||||||
|
log.info('handshake to socket done, authenticating')
|
||||||
// TODO authenticate here by encrypting a payload with private key and sending that.
|
// TODO authenticate here by encrypting a payload with private key and sending that.
|
||||||
// await btc(authenticate)
|
// await btc(authenticate)
|
||||||
|
this.emit('connected') // for end users to take action
|
||||||
resolve(res)
|
resolve(res)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.on('error', async err => {
|
let reconTimeout
|
||||||
log.warn({ error: err.code }, `connect error ${err.code}`)
|
// function that sets a reconnect timeout
|
||||||
if (err.code === 'EISCONN') {
|
const reconnect = () => {
|
||||||
return resolve('ready')
|
reconTimeout = setTimeout(() => {
|
||||||
}
|
this.removeAllListeners()
|
||||||
|
this.stream.removeAllListeners()
|
||||||
reconnect = setTimeout(() => {
|
this.destroy()
|
||||||
connect()
|
connect()
|
||||||
}, this.wait * 1000)
|
}, this.wait * 1000)
|
||||||
})
|
}
|
||||||
|
|
||||||
this.on('end', async () => {
|
|
||||||
log.warn('socket (server) terminated unexpectantly')
|
// connection function that sets listeners and deals with reconnect
|
||||||
if (this.keepAlive) {
|
const connect = () => {
|
||||||
log.info(
|
if (this.opts.host === '127.0.0.1'|| this.opts.host ==='localhost')
|
||||||
'keep alive was set, so waiting on server to come online for reconnect'
|
log.warn('tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead')
|
||||||
)
|
|
||||||
this.destroy()
|
if(!initial) {
|
||||||
this.emit('error', { code: 'DISCONNECTED' })
|
this.once('connect', async () => {
|
||||||
|
clearTimeout(reconTimeout)
|
||||||
|
this._listen()
|
||||||
|
log.info({msg:'reconnected waiting for socket ready handshake'})
|
||||||
|
this.setKeepAlive(this.keepAlive, 3000)
|
||||||
|
let [err, res] = await btc(isReady).bind(this)(this.__ready,this.wait,this.timeout)
|
||||||
|
if (err) reject(err)
|
||||||
|
log.info('rehandshake done, reauthenticating')
|
||||||
|
// TODO authenticate here by encrypting a payload with private key and sending that.
|
||||||
|
// await btc(authenticate)
|
||||||
|
this.emit('reconnected') // for end users to take action
|
||||||
|
resolve(res)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
this.on('error', async err => {
|
||||||
|
if (err.code !== 'EISCONN') {
|
||||||
|
this._ready = false
|
||||||
|
this.emit('ready', false)
|
||||||
|
log.warn({ error: err.code }, `connect error ${err.code}, attempting reconnect`)
|
||||||
|
reconnect()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._ready = true
|
||||||
|
this.emit('ready', true)
|
||||||
|
log.info('reconnected to socket, ready to go again')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.keepAlive) { // only attempt reconnect is keepAlive is set which it is by default
|
||||||
|
this.on('end', async () => {
|
||||||
|
log.warn('socket (server) terminated unexpectantly')
|
||||||
|
this._ready = false
|
||||||
|
log.info('keep alive was set, so waiting on server to come online for reconnect')
|
||||||
|
this.emit('error', { code: 'DISCONNECTED' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt connection
|
||||||
|
log.info({ opts: this.opts, msg:`attempting to connect ${this.id} to socket`})
|
||||||
|
super.connect(this.opts)
|
||||||
|
|
||||||
|
} // end connect function
|
||||||
|
|
||||||
connect() // initial connect request
|
connect() // initial connect request
|
||||||
}) //end promise
|
}) //end promise
|
||||||
|
@ -130,11 +157,9 @@ class SocketConsumer extends Socket {
|
||||||
|
|
||||||
async send(ipacket) {
|
async send(ipacket) {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
// need this for when multiple sends for different consumers use same packet instance
|
if (!this._ready) resolve({ error: 'socket consumer not connected, aborting send' })
|
||||||
let packet = Object.assign({}, ipacket)
|
let packet = Object.assign({}, ipacket) // need to avoid mutuation for different consumers using same packet instance
|
||||||
setTimeout(() => {
|
setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000)
|
||||||
resolve({ error: 'no response from socket in 10sec' })
|
|
||||||
}, 10000)
|
|
||||||
packet._header = {
|
packet._header = {
|
||||||
id: Math.random()
|
id: Math.random()
|
||||||
.toString()
|
.toString()
|
||||||
|
@ -146,24 +171,20 @@ class SocketConsumer extends Socket {
|
||||||
}
|
}
|
||||||
let [err, res] = await btc(this.stream.serialize)(packet)
|
let [err, res] = await btc(this.stream.serialize)(packet)
|
||||||
if (err)
|
if (err)
|
||||||
resolve({
|
resolve({error: 'unable to serialize packet for sending',packet: packet})
|
||||||
error: 'unable to serialize packet for sending',
|
|
||||||
packet: packet
|
|
||||||
})
|
|
||||||
await this.__write(res)
|
await this.__write(res)
|
||||||
this.once(packet._header.id, async function(reply) {
|
this.once(packet._header.id, async function(reply) {
|
||||||
let res = await this._packetProcess(reply)
|
let res = await this._packetProcess(reply)
|
||||||
if (!res) {
|
if (!res) { // if packetProcess was not promise
|
||||||
// if process was not promise returning like just logged to console
|
|
||||||
res = reply
|
res = reply
|
||||||
// log.warn('consumer function was not promise returning further processing may be out of sequence')
|
log.warn('consumer function was not promise returning further processing may be out of sequence')
|
||||||
}
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
}) //end listener
|
}) //end listener
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO register alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks)
|
// TODO register user alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks)
|
||||||
// TODO register authenciation function (set up default)
|
// TODO register authenciation function (set up default)
|
||||||
|
|
||||||
registerPacketProcessor(func) {
|
registerPacketProcessor(func) {
|
||||||
|
@ -195,7 +216,7 @@ class SocketConsumer extends Socket {
|
||||||
// TODO do some extra security here?
|
// TODO do some extra security here?
|
||||||
let res = await this._packetProcess(packet)
|
let res = await this._packetProcess(packet)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
// if process was not promise returning like just logged to console
|
// if process was not promise returning then res will be undefined
|
||||||
log.warn('consumer packet processing function was not promise returning')
|
log.warn('consumer packet processing function was not promise returning')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -204,7 +225,7 @@ class SocketConsumer extends Socket {
|
||||||
this.stream.on('message', messageProcess.bind(this))
|
this.stream.on('message', messageProcess.bind(this))
|
||||||
|
|
||||||
async function messageProcess(packet) {
|
async function messageProcess(packet) {
|
||||||
// console.log('incoming packet from socket',packet)
|
log.debug('incoming packet from socket',packet)
|
||||||
if (packet._handshake) {
|
if (packet._handshake) {
|
||||||
this._ready = true
|
this._ready = true
|
||||||
return
|
return
|
||||||
|
@ -217,9 +238,8 @@ class SocketConsumer extends Socket {
|
||||||
|
|
||||||
// default packet process just a simple console logger. ignores any cmd: prop
|
// default packet process just a simple console logger. ignores any cmd: prop
|
||||||
_packetProcess(packet) {
|
_packetProcess(packet) {
|
||||||
console.log(
|
console.log('default consumer processor -- log packet from socket to console')
|
||||||
'default consumer processor -- log packet from socket to console'
|
console.log('replace by calling .registerPacketProcessor(func) with your function')
|
||||||
)
|
|
||||||
console.dir(packet)
|
console.dir(packet)
|
||||||
}
|
}
|
||||||
} // end class
|
} // end class
|
||||||
|
|
|
@ -239,7 +239,7 @@ export default function socketClass(Server) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// must have a consumer socket bound to use
|
// consumer send, must have a consumer socket bound to use
|
||||||
async _send(packet) {
|
async _send(packet) {
|
||||||
// timeout already set if sockect can't be drained in 10 secs
|
// timeout already set if sockect can't be drained in 10 secs
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
|
Loading…
Reference in New Issue