UNPKG

jspurefix

Version:
474 lines (422 loc) 16.7 kB
import { ElasticBuffer, MsgView } from '../../buffer' import { IJsFixConfig, IJsFixLogger } from '../../config' import { FixSessionState } from './fix-session-state' import { MsgTransport } from '../factory' import { MsgTag } from '../../types' import { ILooseObject } from '../../collections/collection' import * as events from 'events' import { SessionState } from './session-state' import { SegmentType } from '../../buffer/segment/segment-type' export abstract class FixSession extends events.EventEmitter { public logReceivedMsgs: boolean = false protected timer: NodeJS.Timeout | null = null protected transport: MsgTransport | null = null public manageSession: boolean = true public checkMsgIntegrity: boolean = false protected readonly me: string protected readonly initiator: boolean protected readonly acceptor: boolean protected readonly sessionState: FixSessionState protected readonly sessionLogger: IJsFixLogger protected requestLogoutType: string protected respondLogoutType: string protected requestLogonType: string protected constructor (public readonly config: IJsFixConfig) { super() const description = config.description this.me = description?.application?.name ?? 'me' this.sessionState = new FixSessionState( { heartBeat: config.description.HeartBtInt, lastPeerMsgSeqNum: config.description.LastReceivedSeqNum }) this.sessionLogger = config.logFactory.logger(`${this.me}:FixSession`) this.initiator = description?.application?.type === 'initiator' this.acceptor = !this.initiator this.checkMsgIntegrity = this.acceptor this.sessionState.compId = description.SenderCompId } stateStr (theState: SessionState): string { return SessionState[theState] } assignState (newState: SessionState): void { const currentState = this.sessionState.state const currentStateStr = this.stateStr(currentState) const logger = this.sessionLogger const msg = `current state ${currentStateStr} (${currentState}) moves to ${SessionState[newState]} (${newState})` logger.info(msg) this.sessionState.state = newState } public setState (state: SessionState): void { const logger = this.sessionLogger const currentState = this.sessionState.state const currentStateStr = this.stateStr(currentState) if (state === currentState) return switch (currentState) { case SessionState.ConfirmingLogout: case SessionState.Stopped: if (state !== SessionState.NetworkConnectionEstablished) { logger.info(`ignoring request to change state as now already in ${currentStateStr}`) } else { this.assignState(state) } break default: { this.assignState(state) } } } public getState (): SessionState { return this.sessionState.state } public lastSentSeqNum (): number { return this.sessionState.lastSentSeqNum() } public lastPeerSeqNum (): number { return this.sessionState.lastPeerMsgSeqNum } public sendLogon (): void { const lo = this.config.factory?.logon() if (lo) { this.send(this.requestLogonType, lo) } } private async waitPromise (): Promise<number> { const logger = this.sessionLogger return await new Promise<any>((resolve, reject) => { if (this.initiator) { logger.debug(`initiator sending logon state = ${this.stateString()}`) this.sendLogon() this.setState(SessionState.InitiationLogonSent) } else { logger.debug(`acceptor waits for logon state = ${this.stateString()}`) this.setState(SessionState.WaitingForALogon) } this.on('error', (e: Error) => { logger.error(e) reject(e) }) this.on('done', () => { resolve(this.transport?.id) }) }) } public async run (transport: MsgTransport): Promise<number> { const logger = this.sessionLogger if (this.transport) { logger.info(`reset from previous transport. state ${this.stateString()}`) this.reset() } this.transport = transport this.subscribe() return await this.waitPromise() } protected expectedEndState (): boolean { switch (this.sessionState.state) { case SessionState.Stopped: case SessionState.ConfirmingLogout: return true default: return false } } protected rxOnEnd (): void { const logger = this.sessionLogger logger.info(`rx end received sessionState = [${this.sessionState.toString()}]`) const expectedState = this.expectedEndState() if (expectedState) { logger.info(`rx receives end state = ${this.stateString()} - stop session`) this.stop() } else { this.setState(SessionState.DetectBrokenNetworkConnection) const e = new Error(`unexpected state - transport failed? = ${this.stateString()}`) logger.info(`rx error ${e.message}`) this.terminate(e) } } protected rxOnMsg (msgType: string, view: MsgView): void { const logger = this.sessionLogger if (this.logReceivedMsgs) { const name = view.segment.type !== SegmentType.Unknown ? view?.segment?.set?.name : 'unknown' logger.info(`${msgType}: ${name}`) logger.info(`${view.toString()}`) } this.sessionState.lastReceivedAt = new Date() if (this.manageSession) { this.onMsg(msgType, view) } else { this.checkForwardMsg(msgType, view) } } protected rxOnDone (): void { const logger = this.sessionLogger logger.info('rx done received') this.done() } protected rxOnError (e: Error): void { const logger = this.sessionLogger logger.warning(`rx error event: ${e.message} ${JSON.stringify(e)}`) this.terminate(e) } protected rxOnDecoded (msgType: string, data: ElasticBuffer, ptr: number): void { const logger = this.sessionLogger logger.debug(`rx: [${msgType}] ${ptr} bytes`) this.onDecoded(msgType, data.toString(ptr)) } protected txOnError (e: Error): void { const logger = this.sessionLogger logger.warning(`tx error event: ${e.message} ${JSON.stringify(e)}`) this.terminate(e) } protected txOnEncoded (msgType: string, data: string, hdr: ILooseObject): void { const logger = this.sessionLogger this.sessionState.lastHeader = hdr logger.debug(`tx: [${msgType}] ${data.length} bytes seqNo = ${this.lastSentSeqNum()}`) this.onEncoded(msgType, data) } protected unsubscribe (): void { const logger = this.sessionLogger logger.info(`unsubscribe sessionState = [${this.sessionState.toString()}]`) const transport = this.transport const rx = transport?.receiver const tx = transport?.transmitter rx?.removeListener('msg', this.rxOnMsg) rx?.removeListener('error', this.rxOnError) rx?.removeListener('done', this.rxOnDone) rx?.removeListener('end', this.rxOnEnd) rx?.removeListener('decoded', this.rxOnDecoded) tx?.removeListener('error', this.txOnError) tx?.removeListener('encoded', this.txOnEncoded) } protected subscribe (): void { const transport = this.transport const rx = transport?.receiver const tx = transport?.transmitter const inst = this rx?.on('msg', (msgType: string, view: MsgView) => { inst.rxOnMsg(msgType, view) }) rx?.on('error', (e: Error) => { inst.rxOnError(e) }) rx?.on('done', () => { inst.rxOnDone() }) rx?.on('end', () => { inst.rxOnEnd() }) rx?.on('decoded', (msgType: string, data: ElasticBuffer, ptr: number) => { inst.rxOnDecoded(msgType, data, ptr) }) tx?.on('error', (e: Error) => { inst.txOnError(e) }) tx?.on('encoded', (msgType: string, data: string, hdr: ILooseObject) => { inst.txOnEncoded(msgType, data, hdr) }) } protected validStateApplicationMsg (): boolean { switch (this.sessionState.state) { case SessionState.Idle: case SessionState.InitiateConnection: case SessionState.InitiationLogonSent: case SessionState.WaitingForALogon: case SessionState.HandleResendRequest: case SessionState.AwaitingProcessingResponseToTestRequest: case SessionState.AwaitingProcessingResponseToResendRequest: return false default: return true } } protected stateString (): string { return SessionState[this.sessionState.state] } protected checkForwardMsg (msgType: string, view: MsgView): void { this.sessionLogger.info(`forwarding msgType = '${msgType}' to application`) this.setState(SessionState.ActiveNormalSession) this.onApplicationMsg(msgType, view) } private stopTimer (): void { if (this.timer) { this.sessionLogger.info('stopTimer') clearInterval(this.timer) this.timer = null } } protected terminate (error: Error): void { if (this.sessionState.state === SessionState.Stopped) return this.sessionLogger.error(error) this.stopTimer() if (this.transport) { this.transport.end() } this.transport = null this.setState(SessionState.Stopped) this.emit('error', error) } protected peerLogout (view: MsgView): void { const msg = view.getString(MsgTag.Text) const state = this.sessionState.state switch (state) { case SessionState.WaitingLogoutConfirm: { this.sessionLogger.info(`peer confirms logout Text = '${msg}'`) this.stop() break } case SessionState.InitiationLogonResponse: case SessionState.ActiveNormalSession: case SessionState.InitiationLogonReceived: { this.setState(SessionState.ConfirmingLogout) this.sessionLogger.info(`peer initiates logout Text = '${msg}'`) this.sessionLogout() } } } protected send (msgType: string, obj: ILooseObject): void { const state = this.sessionState.state switch (state) { case SessionState.Stopped: { this.sessionLogger.warning(`can't send in state ${this.stateString()}`) break } default: { this.sessionState.LastSentAt = new Date() this.transport?.transmitter.send(msgType, obj) break } } } protected sendLogout (msg: string): void { const factory = this.config.factory this.sessionLogger.info(`sending logout with ${msg}`) const lo = factory?.logout(this.requestLogoutType, msg) if (lo) { this.send(this.requestLogoutType, lo) } } protected sessionLogout (): void { const sessionState = this.sessionState if (sessionState.logoutSentAt) { return } switch (sessionState.state) { case SessionState.ActiveNormalSession: case SessionState.InitiationLogonResponse: case SessionState.InitiationLogonReceived: { // this instance initiates logout this.setState(SessionState.WaitingLogoutConfirm) sessionState.logoutSentAt = new Date() const msg = `${this.me} initiate logout` this.sessionLogger.info(msg) this.sendLogout(msg) break } case SessionState.ConfirmingLogout: { // this instance responds to log out this.setState(SessionState.ConfirmingLogout) sessionState.logoutSentAt = new Date() const msg = `${this.me} confirming logout` this.sessionLogger.info(msg) this.sendLogout(msg) break } default: { this.sessionLogger.info(`sessionLogout ignored as in state ${sessionState.state}`) } } } public done (): void { switch (this.sessionState.state) { case SessionState.InitiationLogonResponse: case SessionState.ActiveNormalSession: case SessionState.InitiationLogonReceived: { this.sessionLogout() break } case SessionState.Stopped: this.sessionLogger.info('done. session is now stopped') break default: { this.stop() break } } this.sessionLogger.info(`done. check logout sequence state ${this.stateString()}`) } public reset (resetSeqNum?: number | null): void { this.stopTimer() this.transport = null const resetFlag = this.config.description.ResetSeqNumFlag const seqNum = resetFlag ? 0 : resetSeqNum ?? this.sessionState.lastPeerMsgSeqNum this.sessionState.reset(seqNum) // from header def ... eventually this.setState(SessionState.NetworkConnectionEstablished) } protected stop (error: Error | null = null): void { if (this.sessionState.state === SessionState.Stopped) { return } this.stopTimer() this.unsubscribe() this.sessionLogger.info('stop: kill transport') this.transport?.end() if (error) { this.sessionLogger.info(`stop: emit error ${error.message}`) this.emit('error', error) } else { this.emit('done') } this.setState(SessionState.Stopped) this.onStopped(error ?? undefined) this.transport = null } /** * dispatches a message into the subclass that inherits from FixSession. The view contains * the parsed message which has utility methods such as toObject(). The Ascii session provides * an implementation to handle admin level messages such as logon, hearbeat and resest request. * Any application messges are dispatched via onApplicationMsg where the application can action * the message. * @param msgType the string based msg type the view represents * @param view container for all parsed fields representing the received message. * @protected */ protected abstract onMsg (msgType: string, view: MsgView): void /** * the parsed txt recieved from the peer application. Given the applicaton is * responible for maintaining the fix log, this can be used to persist all received * messages. * @param msgType the string based msg type the view represents * @param txt the received message where for Ascii, the wire SOH delimeter is replaced * with that specified in the config e.g. '|' * @protected */ protected abstract onDecoded (msgType: string, txt: string): void /** * the formatted txt sent to the peer application as an outbound message. Given the applicaton is * responible for maintaining the fix log, this can be used to persist all transmitted * messages. use msgType for example to persist only trade capture messages to database * @param msgType the msg type representing the message. * @param txt the sent message where for Ascii, the wire SOH delimeter is replaced * with that specified in the config e.g. '|' * @protected */ protected abstract onEncoded (msgType: string, txt: string): void /** * typically all session level messages are handled by AsciiSession and these are * application level such as MarketDataRefresh. This will represent the applications main * work functiono where responses can be sent back to the peer. If manageSession has been set false * (not recommended) all messages are sent to this function * @param msgType the msg type representing the message. * @param view a wrapper containing the parsed message received. * @protected */ protected abstract onApplicationMsg (msgType: string, view: MsgView): void /** * at this point the application is ready to send messages - peer login has been achieved * and the session can be considered ready to use. In the case of an initiator the application * may at this point send for security definitions or send market data subscriptions * @param view the login message causing session to be ready * @protected */ protected abstract onReady (view: MsgView): void /** * Inform application this session has now ended - either from logout or connection dropped * @param error if session has been terminated via an error it is provided * @protected */ protected abstract onStopped (error?: Error): void /** * Placeholder infomring the application of a peer login attempt. * @param view the login message * @param user extracted user from message * @param password extracted password from the message. * @protected */ protected abstract onLogon (view: MsgView, user: string, password: string): boolean }