jspurefix
Version:
pure node js fix engine
206 lines (191 loc) • 7.02 kB
text/typescript
import { FixInitiator } from '../fix-initiator'
import { MsgTransport } from '../factory'
import { IJsFixConfig, IJsFixLogger } from '../../config'
import { TcpDuplex, FixDuplex } from '../duplex'
import * as util from 'util'
import { connect as tlsConnect, ConnectionOptions, TLSSocket } from 'tls'
import { createConnection } from 'net'
import Timeout = NodeJS.Timeout
import { TlsOptionsFactory } from './tls-options-factory'
import { inject, injectable } from 'tsyringe'
import { DITokens } from '../../runtime/di-tokens'
import { ITcpTransportDescription } from './tcp-transport-description'
export enum InitiatorState {
Idle = 1,
Connecting = 2,
Connected = 3,
Stopped = 4
}
()
export class TcpInitiator extends FixInitiator {
public tcp: ITcpTransportDescription | null
public state: InitiatorState = InitiatorState.Idle
private readonly logger: IJsFixLogger
private duplex: FixDuplex
private th: Timeout | null = null
constructor ((DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
super(jsFixConfig.description.application ?? null)
const name = this.application?.name ?? 'initiator'
this.logger = jsFixConfig.logFactory.logger(`${name}:TcpInitiator`)
if (!this.application) {
throw new Error('no application in session description.')
}
this.tcp = this.application.tcp ?? null
if (!this.tcp) {
throw new Error('no tcp in session description need tcp { host: hostname, port: port }')
}
}
public end (): void {
this.clearTimer()
switch (this.state) {
case InitiatorState.Connected: {
this.logger.info('end')
this.duplex.end()
this.state = InitiatorState.Stopped
break
}
default: {
this.logger.info(`end :state ${this.state}`)
this.state = InitiatorState.Stopped
break
}
}
}
public async connect (timeoutSeconds: number): Promise<MsgTransport> {
return await new Promise<MsgTransport>(async (resolve, reject) => {
switch (this.state) {
case InitiatorState.Idle: {
this.state = InitiatorState.Connecting
this.logger.info(`connecting with timeout ${timeoutSeconds}`)
this.tryConnect()
.then((t: MsgTransport) => { resolve(t) })
.catch((_: Error) => {
this.repeatConnect(timeoutSeconds)
.then((t: MsgTransport) => { resolve(t) })
.catch((e: Error) => { reject(e) })
})
break
}
default: {
const e: Error = new Error('connect not valid from non idle state')
this.logger.warning(`rejecting from state ${this.state}`)
reject(e)
}
}
})
}
private async unsecureDuplex (): Promise<TcpDuplex> {
const tcp = this.tcp
return await new Promise<TcpDuplex>((resolve, reject) => {
try {
this.logger.info('unsecureDuplex try to connect to endPoint')
const socket = tcp
? createConnection(tcp, () => {
try {
this.logger.info('net.createConnection cb, resolving')
if (socket) {
const tcpDuplex = new TcpDuplex(socket)
resolve(tcpDuplex)
} else {
reject(new Error('no socket in tcp initiator'))
}
} catch (e) {
reject(e)
}
})
: null
socket?.on('error', (err) => {
reject(err)
})
} catch (e) {
reject(e)
}
})
}
private async tlsDuplex (): Promise<TcpDuplex > {
return await new Promise<TcpDuplex>((resolve, reject) => {
let tlsSocket: TLSSocket | null = null
const tcp = this.tcp
const connectionOptions: ConnectionOptions | null = tcp ? TlsOptionsFactory.getTlsConnectionOptions(tcp) : null
if (connectionOptions) {
try {
tlsSocket = tlsConnect(connectionOptions, () => {
if (!tlsSocket) return null
this.logger.info(`client connected ${tlsSocket.authorized ? 'authorized' : 'unauthorized'}`)
if (!tlsSocket.authorized) {
const error = tlsSocket.authorizationError
this.logger.warning(`rejecting from state ${this.state} authorizationError ${error}`)
tlsSocket.end()
reject(error)
} else {
tlsSocket.setEncoding('utf8')
const tlsDuplex = new TcpDuplex(tlsSocket)
if (tcp?.tls?.enableTrace) {
this.logger.info('enabling tls session trace')
tlsSocket.enableTrace()
}
this.logger.info('tlsDuplex resolving')
resolve(tlsDuplex)
}
})
tlsSocket.on('error', (err) => {
reject(err)
})
} catch (e) {
reject(e)
}
}
})
}
private async tryConnect (): Promise < MsgTransport > {
return await new Promise<MsgTransport>((resolve, reject) => {
const tcp = this.tcp
const connectionOptions: ConnectionOptions | null = tcp ? TlsOptionsFactory.getTlsConnectionOptions(tcp) : null
const connector = connectionOptions ? this.tlsDuplex() : this.unsecureDuplex()
this.logger.info(`tryConnect ${tcp?.host}:${tcp?.port}`)
connector.then(duplex => {
this.duplex = duplex
resolve(new MsgTransport(0, this.jsFixConfig, duplex))
}).catch(e => {
reject(e)
})
})
}
public clearTimer (): void {
if (this.th) {
clearInterval(this.th)
this.th = null
}
}
private async repeatConnect (timeoutSeconds: number): Promise < MsgTransport > {
return await new Promise<MsgTransport>(async (resolve, reject) => {
const application = this.application
const promisify = util.promisify
const timeoutPromise = promisify(setTimeout)
const reconnectSeconds = application?.reconnectSeconds ?? 5
let retries = 0
let lastError: Error
const name = application?.name ?? 'initiator'
this.th = setInterval(() => {
++retries
this.tryConnect()
.then((t: MsgTransport) => {
this.state = InitiatorState.Connected
this.clearTimer()
resolve(t)
}).catch((e: Error) => {
lastError = e
this.logger.info(`${name}: retries ${retries} ${e.message}`)
})
}, reconnectSeconds * 1000)
timeoutPromise(timeoutSeconds * 1000).then(() => {
this.clearTimer()
this.state = InitiatorState.Stopped
const e = lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`)
reject(e)
}).catch(e => {
reject(e)
})
})
}
}