UNPKG

jspurefix

Version:
376 lines (331 loc) 13.5 kB
import { MsgView } from '../../buffer' import { MsgTag, MsgType, SessionRejectReason } from '../../types' import { IJsFixConfig } from '../../config' import { FixSession } from '../session/fix-session' import { FixMsgAsciiStoreResend, FixMsgMemoryStore, IFixMsgStore, IFixMsgStoreRecord } from '../../store' import { SessionState } from '../tcp' import { TickAction } from '../tick-action' import { IMsgApplication } from '../msg-application' import { SegmentType } from '../../buffer/segment/segment-type' export abstract class AsciiSession extends FixSession { public heartbeat: boolean = true protected store: IFixMsgStore | null = null protected resender: FixMsgAsciiStoreResend protected constructor (public readonly config: IJsFixConfig) { super(config) this.requestLogoutType = this.respondLogoutType = MsgType.Logout this.requestLogonType = MsgType.Logon this.store = new FixMsgMemoryStore(this.config.description.SenderCompId, this.config) this.resender = new FixMsgAsciiStoreResend(this.store, this.config) } private checkSeqNo (msgType: string, view: MsgView): boolean { switch (msgType) { case MsgType.SequenceReset: { return true } default: { const state = this.sessionState const lastSeq: number = state.lastPeerMsgSeqNum const seqNo: number = view.getTyped(MsgTag.MsgSeqNum) as number let ret: boolean = false const seqDelta: number = seqNo - lastSeq if (seqDelta <= 0) { // serious problem ... drop immediately this.sessionLogger.warning(`terminate as seqDelta (${seqDelta}) < 0 lastSeq = ${lastSeq} seqNo = ${seqNo}`) this.stop() } else if (seqDelta > 1) { // resend request required as have missed messages. // We process a Logon beforehand to confirm the connection even we out of sync if (msgType === MsgType.Logon) { this.peerLogon(view) } // If the out of sync message is a resend request itself, then we handle it first in order // to avoid triggering an endless loop of both sides sending resend requests in response to resend requests. if (msgType === MsgType.ResendRequest) { this.onResendRequest(view) } this.sendResendRequest(lastSeq, seqNo) } else { ret = true state.lastPeerMsgSeqNum = seqNo } return ret } } } protected checkForwardMsg (msgType: string, view: MsgView): void { const okToForward = this.validStateApplicationMsg() if (okToForward) { this.sessionLogger.info(`ascii forwarding msgType = '${msgType}' to application`) this.setState(SessionState.ActiveNormalSession) this.onApplicationMsg(msgType, view) } else { this.terminate(new Error(`msgType ${msgType} received in state ${this.stateString()}`)) } } private sendReject (msgType: string, seqNo: number, msg: string, reason: number): void { const factory = this.config.factory const reject = factory?.reject(msgType, seqNo, msg, reason) if (reject) { this.sessionLogger.warning(`rejecting with ${JSON.stringify(reject)}`) this.send(MsgType.Reject, reject) } } protected sendResendRequest (lastSeq: number, receivedSeq: number): void { const resend = this.config.factory?.resendRequest(lastSeq + 1, 0) if (resend) { this.sessionLogger.warning(`received seq ${receivedSeq}, but last known seq is ${lastSeq}. Sending resend request for all messages > ${lastSeq}`) this.send(MsgType.ResendRequest, resend) } } private checkIntegrity (msgType: string, view: MsgView): boolean { const state = this.sessionState const seqNum = view.getTyped(MsgTag.MsgSeqNum) as number const received: number = parseInt(view.getString(MsgTag.CheckSum) ?? '', 10) const computed = view.checksum() if (received !== computed) { const msg: string = `msgType ${msgType} checksum failed. received = ${received} computed = ${computed}` this.sendReject(msgType, seqNum, msg, SessionRejectReason.ValueIsIncorrect) return false } if (view.segment.type === SegmentType.Unknown) { const msg: string = `msgType ${msgType} unknown` this.sendReject(msgType, seqNum, msg, SessionRejectReason.InvalidMsgType) return false } const invalid = view.invalid() if (invalid.length > 0) { const msg: string = `msgType ${msgType} invalid tag${invalid.length > 1 ? 's' : ''} ${invalid.join(', ')}` this.sendReject(msgType, seqNum, msg, SessionRejectReason.InvalidTagNumber) return false } const undefinedMsg: string | null = view.undefinedForMsg() if (undefinedMsg) { const msg: string = `msgType ${msgType} ${undefinedMsg}` this.sendReject(msgType, seqNum, msg, SessionRejectReason.TagNotDefinedForThisMessageType) return false } const missingRequired = view.missing() if (missingRequired.length > 0) { const msg: string = `msgType ${msgType} missing required tag${missingRequired.length > 1 ? 's' : ''} ${missingRequired.join(', ')}` this.sendReject(msgType, seqNum, msg, SessionRejectReason.RequiredTagMissing) return false } switch (state.state) { case SessionState.InitiationLogonReceived: case SessionState.InitiationLogonResponse: { const targetCompId = view.getString(MsgTag.TargetCompID) if (targetCompId !== state.compId) { const msg: string = `msgType ${msgType} unexpected TargetCompID ${targetCompId} expecting ${state.compId})` this.sendReject(msgType, seqNum, msg, SessionRejectReason.CompIDProblem) return false } const peerCompId = view.getString(MsgTag.SenderCompID) if (peerCompId !== state.peerCompId) { const msg: string = `msgType ${msgType} unexpected SenderCompID ${peerCompId} expecting ${state.compId}` this.sendReject(msgType, seqNum, msg, SessionRejectReason.CompIDProblem) return false } } break default: { break } } return true } /** * Override to resend stored messages following a sequence reset. * @protected */ protected onResendRequest (view: MsgView): void { // if no records are in store then send a gap fill for entire sequence this.setState(SessionState.HandleResendRequest) const [beginSeqNo, requestedEndSeqNo] = view.getTypedTags([MsgTag.BeginSeqNo, MsgTag.EndSeqNo]) const endSeqNo = requestedEndSeqNo === 0 ? this.sessionState.lastSentSeqNum() : requestedEndSeqNo this.sessionLogger.info(`onResendRequest getResendRequest beginSeqNo = ${beginSeqNo}, endSeqNo = ${endSeqNo}`) this.resender.getResendRequest(beginSeqNo as number, endSeqNo as number).then((records: IFixMsgStoreRecord[]) => { const validRecords = records.filter(rec => rec.obj !== null) this.sessionLogger.info(`sending ${validRecords.length}`) validRecords.forEach(rec => { if (rec.obj) { this.send(rec.msgType, rec.obj) } }) this.setState(SessionState.ActiveNormalSession) }).catch((e: Error) => { this.sessionLogger.error(e) }) } okForLogon (): boolean { const state = this.sessionState.state if (this.acceptor) { return state === SessionState.WaitingForALogon } return state === SessionState.InitiationLogonSent } protected onSessionMsg (msgType: string, view: MsgView): void { const logger = this.sessionLogger switch (msgType) { case MsgType.Logon: { // only valid to receive a logon when in LogonSent or WaitingALogon // else will drop connection immediately. if (this.okForLogon()) { this.peerLogon(view) } else { this.terminate(new Error(`state ${this.stateString()} is illegal for Logon`)) } break } case MsgType.Logout: { this.peerLogout(view) break } case MsgType.TestRequest: { const req: string | null = view.getString(MsgTag.TestReqID) if (req) { this.sendHeartbeat(req) } break } case MsgType.Heartbeat: { this.sessionState.lastTestRequestAt = null this.setState(SessionState.ActiveNormalSession) break } case MsgType.ResendRequest: { logger.info(`peer sends '${msgType}' resend request.`) this.onResendRequest(view) break } case MsgType.SequenceReset: { const newSeqNo: number = view.getTyped(MsgTag.NewSeqNo) as number logger.info(`peer sends '${msgType}' sequence reset. newSeqNo = ${newSeqNo}`) // expect newSeqNo to be the next message's sequence number. this.sessionState.lastPeerMsgSeqNum = newSeqNo - 1 break } case MsgType.Reject: { logger.info(`peer rejects type '${msgType}' with text '${view.getTyped(MsgTag.Text)}'`) break } } } protected onMsg (msgType: string, view: MsgView): void { if (!this.checkSeqNo(msgType, view)) { this.sessionLogger.info(`message '${msgType}' failed checkSeqNo.`) return } if (this.checkMsgIntegrity && !this.checkIntegrity(msgType, view)) { this.sessionLogger.info(`message '${msgType}' failed checkIntegrity.`) switch (msgType) { case MsgType.Logon: { this.setState(SessionState.PeerLogonRejected) this.startTimer() break } } return } switch (msgType) { case MsgType.Logon: case MsgType.Logout: case MsgType.TestRequest: case MsgType.Reject: case MsgType.SequenceReset: case MsgType.Heartbeat: case MsgType.ResendRequest: { this.onSessionMsg(msgType, view) break } default: { this.checkForwardMsg(msgType, view) break } } } private startTimer (interval: number = 200): void { const logger = this.sessionLogger logger.info(`start heartbeat timer. interval = ${interval}`) this.timer = setInterval(() => { this.tick() }, interval) } private peerLogon (view: MsgView): void { const logger = this.sessionLogger const [heartBtInt, peerCompId, userName, password] = view.getTypedTags([MsgTag.HeartBtInt, MsgTag.SenderCompID, MsgTag.Username, MsgTag.Password]) logger.info(`peerLogon Username = ${userName}, heartBtInt = ${heartBtInt}, peerCompId = ${peerCompId}, userName = ${userName}`) const state = this.sessionState state.peerHeartBeatSecs = view.getTyped(MsgTag.HeartBtInt) as number state.peerCompId = view.getTyped(MsgTag.SenderCompID) as string const res = this.onLogon(view, userName as string, password as string) // currently not using this. logger.info(`peerLogon onLogon returns ${res}`) if (this.acceptor) { this.setState(SessionState.InitiationLogonResponse) logger.info('acceptor responds to logon request') this.sendLogon() // if res send response else reject, terminate } else { // as an initiator the acceptor has responded logger.info('initiator receives logon response') this.setState(SessionState.InitiationLogonReceived) } if (this.heartbeat) { this.startTimer() } logger.info('system ready, inform app') this.onReady(view) } private sendTestRequest (): void { const factory = this.config.factory this.setState(SessionState.AwaitingProcessingResponseToTestRequest) const tr = factory?.testRequest() if (tr) { this.send(MsgType.TestRequest, tr) } } private sendHeartbeat (testReqId: string): void { const factory = this.config.factory const hb = factory?.heartbeat(testReqId) if (hb) { this.send(MsgType.Heartbeat, hb) } } private tick (): void { if (!this.transport) return const sessionState = this.sessionState const action: TickAction = sessionState.calcAction(new Date()) const application: IMsgApplication | null = this.transport.config.description.application ?? null const logger = this.sessionLogger switch (action) { case TickAction.Nothing: { // all is well break } case TickAction.TestRequest: { logger.debug(`send test req. state = ${sessionState.toString()}`) this.sendTestRequest() break } case TickAction.Heartbeat: { logger.debug(`send heartbeat. state = ${sessionState.toString()}`) this.sendHeartbeat(sessionState.now.toUTCString()) break } case TickAction.TerminateOnError: { logger.info(sessionState.toString()) this.terminate(new Error(`${application?.name}: peer not responding`)) break } case TickAction.Stop: { logger.info(sessionState.toString()) logger.info('stopping') this.stop() break } default: throw new Error('unexpected action') } } }