jspurefix
Version:
pure node js fix engine
175 lines (158 loc) • 6.79 kB
text/typescript
import { IJsFixConfig, IJsFixLogger } from '../../config'
import { FixSession } from '../session/fix-session'
import { TcpInitiator } from './tcp-initiator'
import { MsgTransport } from '../factory'
import Timeout = NodeJS.Timeout
import { SessionState } from '../session/session-state'
import { inject, injectable } from 'tsyringe'
import { DITokens } from '../../runtime/di-tokens'
import { ITcpTransportDescription } from './tcp-transport-description'
import { IMsgApplication } from '../msg-application'
import { FixEntity } from '../fix-entity'
/*
create one application session instance - and recover a lost transport. Hence, the application
will automatically re-connected and the "message recovery" policy enacted i.e. replay from
last known sequence number or sequence reset.
*/
export class RecoveringTcpInitiator extends FixEntity {
public tcp: ITcpTransportDescription | null
public session: FixSession
private readonly logger: IJsFixLogger
private readonly application: IMsgApplication | null
private initiator: TcpInitiator
private transport: MsgTransport
private th: Timeout | null = null
public recoveryAttemptSecs: number = 5
public backoffFailConnectSecs: number = 30
constructor ( public readonly jsFixConfig: IJsFixConfig) {
super(jsFixConfig)
this.application = this.jsFixConfig.description.application ?? null
const name = this.application?.name ?? 'na'
this.logger = jsFixConfig.logFactory.logger(`${name}:RecoveringTcpInitiator`)
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 }')
}
this.createSession(jsFixConfig)
}
private createSession (jsFixConfig: IJsFixConfig): void {
this.logger.info(`creating an application session with DI token ${DITokens.FixSession}.`)
this.session = jsFixConfig.sessionContainer.resolve<FixSession>(DITokens.FixSession)
this.session.on('done', () => {
this.logger.info('session has permanently ended')
this.emit('end', this)
})
this.session.on('end', () => {
this.logger.info('session has permanently ended')
this.emit('end', this)
})
this.session.setState(SessionState.DisconnectedNoConnectionToday)
}
public getState (): SessionState {
return this.session.getState()
}
public lastSentSeqNum (): number {
return this.session.lastSentSeqNum()
}
private resetSeq (): boolean {
return this.jsFixConfig.description.ResetSeqNumFlag
}
private newTransport (transport: MsgTransport): void {
this.transport = transport
this.emit('transport', transport)
this.logger.info(`initiator connects id ${(transport.id)}`)
const session = this.session
if (this.resetSeq()) {
this.logger.info('reset sequence numbers')
session.reset()
}
session.setState(SessionState.NetworkConnectionEstablished)
session.run(transport).then((id: number) => {
if (!transport || id === transport.id) {
this.emit('end', this)
} else {
this.logger.info(`old transport ${id} ends waiting on ${(transport.id)}`)
}
}).catch(e => {
this.logger.info(`transport id ${(transport.id)} failed - session state ${session.getState()}`)
this.logger.warning(e.message)
this.recover()
})
this.logger.info(`running session with transport ${transport.id} state = ${session.getState()}`)
}
private clearTimer (): void {
if (this.th) {
clearTimeout(this.th)
this.th = null
}
}
// at least one connection was established so retry to establish - either
// succeed in which case can restart session or fails in which case wait and
// restart an attempt to connect
private recover (): void {
this.session.setState(SessionState.DetectBrokenNetworkConnection)
this.logger.info(`recover session transport - attempt in ${this.recoveryAttemptSecs} secs`)
if (!this.resetSeq()) {
const lastSentSeqNum = this.lastSentSeqNum()
if (lastSentSeqNum > 0) {
this.logger.info(`recover session set LastSentSeqNum ${lastSentSeqNum} for new transport`)
this.config.description.LastSentSeqNum = lastSentSeqNum
}
}
this.th = setTimeout(() => {
this.connect(60).then(t => {
this.logger.info(`new transport ${t.id}`)
}).catch((e) => {
this.logger.info(`failed to re-connect ${e.message} - backoff for ${this.backoffFailConnectSecs}`)
this.th = setTimeout(() => {
this.logger.info('returning to recover()')
setImmediate(() => {
this.recover()
})
}, this.backoffFailConnectSecs * 1000)
})
}, this.recoveryAttemptSecs * 1000)
}
public async start (): Promise<any> {
return await this.run()
}
// for first connection - reject if no initial connection established within timeout
// once connection established, will not resolve until session is ended - i.e. lost
// connections are re-established using the same session instance.
public async run (initialTimeout: number = 60): Promise<any> {
return await new Promise<any>((resolve, reject) => {
this.connect(initialTimeout).then(() => {
this.on('end', () => {
this.clearTimer()
this.initiator.end()
this.logger.info(`run: transport ${this.transport.id} gracefully ends ${initialTimeout} - resolving`)
resolve(null)
})
}).catch(e => {
this.logger.info(`run: failed to connect to first transport ${initialTimeout} - rejecting`)
reject(e)
})
})
}
// return a promise for new transport - or reject if no connection within timeout
private async connect (timeout: number): Promise<MsgTransport> {
return await new Promise<MsgTransport>((resolve, reject) => {
this.logger.info(`connect: start initiator timeout ${timeout}`)
this.session.setState(SessionState.InitiateConnection)
this.initiator = new TcpInitiator(this.jsFixConfig)
this.initiator.connect(timeout).then((transport: MsgTransport) => {
this.logger.info(`connect: receive new transport ${transport.id}`)
this.newTransport(transport)
resolve(transport)
}).catch(e => {
this.logger.info(`connect: failed to connect within ${timeout} - rejecting`)
this.session.setState(SessionState.DetectBrokenNetworkConnection)
reject(e)
})
})
}
}