jspurefix
Version:
pure node js fix engine
171 lines (156 loc) • 6.16 kB
text/typescript
import { MsgTransport } from '../factory'
import { FixAcceptor } from '../fix-acceptor'
import { IJsFixConfig, IJsFixLogger } from '../../config'
import { IFixmlRequest } from '../fixml'
import { FixDuplex, StringDuplex, StringDuplexTraits } from '../duplex'
import * as express from 'express'
import * as bodyParser from 'body-parser'
import * as http from 'http'
import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'tsyringe'
import { DITokens } from '../../runtime/di-tokens'
export class HttpAcceptor extends FixAcceptor {
private readonly app: express.Express = express()
private server: http.Server
private readonly logger: IJsFixLogger
private readonly router: express.Router
private nextId: number = 0
private readonly keys: Map<string, MsgTransport> = new Map<string, MsgTransport>()
constructor ( public readonly config: IJsFixConfig) {
super(config?.description?.application ?? null)
this.logger = config.logFactory.logger(`${config?.description?.application?.name}:HttpAcceptor`)
this.logger.info('creating http server')
this.router = express.Router()
this.router.use(bodyParser.json())
this.subscribe()
this.app.use('/', this.router)
}
public listen (): void {
const app = this.config.description.application
const port = app?.http?.port ?? 0
const logger = this.logger
logger.info(`start to listen ${port}`)
this.server = this.app.listen(port, () => {
logger.info(`app listening at http://localhost:${port}${app?.http?.uri}`)
})
this.server.on('error', (err: Error) => {
logger.error(err)
this.emit('error', err)
})
}
public close (callback?: (err?: Error) => void): void {
const app = this.config.description.application
const port = app?.http?.port
this.logger.info(`close listener on port ${port}`)
this.server.close(callback)
}
private saveTransport (tid: number, transport: MsgTransport): string {
this.transports[tid] = transport
const keys: string[] = Object.keys(this.transports)
const a = uuidv4()
this.keys.set(a, transport)
this.logger.info(`new transport id = ${tid} token = ${a} created total transports = ${keys.length}`)
this.emit('transport', transport)
return a
}
private harvestTransport (token: string, tid: number): void {
delete this.transports[tid]
this.keys.delete(token)
const keys: string[] = Object.keys(this.transports)
this.logger.info(`transport ${tid} ends total transports = ${keys.length}`)
}
private async respond (duplex: FixDuplex, response: express.Response, token: string | null = null): Promise<any> {
return new Promise<any>((resolve, reject) => {
response.setHeader('Content-Type', 'application/json')
const timer = setTimeout(() => {
const businessReject = '<FIXML><BizMsgRej BizRejRsn="4" Txt="no response from application"/></FIXML>'
const b = Buffer.from(businessReject, 'utf-8')
duplex.writable.removeListener('data', transmit)
response.send(b)
reject(new Error('no response'))
}, 5000)
const transmit = (d: Buffer): void => {
this.logger.info('responding to request')
clearTimeout(timer)
if (token) {
response.setHeader('authorization', token)
}
duplex.writable.removeListener('data', transmit)
response.send(d)
resolve(true)
}
duplex.writable.on('data', transmit)
})
}
private async logon (req: express.Request, res: express.Response): Promise<void> {
const body: IFixmlRequest = req.body
const id = this.nextId++
this.logger.info(JSON.stringify(body, null, 4))
// check hand back session key
const d = new StringDuplex('', StringDuplexTraits.None)
const transport = new MsgTransport(id, this.config, d)
const token = this.saveTransport(id, transport)
this.respond(d, res, token).then(() => {
this.logger.info('responded to logon')
}).catch((e: Error) => {
this.logger.error(e)
})
d.readable.push(body.fixml)
}
private async logout (req: express.Request, res: express.Response): Promise<void> {
const headers = req.headers
const body: IFixmlRequest = req.body
const t: MsgTransport | undefined = this.keys.get(headers?.authorization ?? '')
if (t) {
const token = req.headers.authorization
if (token) {
this.harvestTransport(token, t.id)
const d = t.duplex
this.respond(d, res, token).then(() => {
this.logger.info('responded to logout')
t.end()
}).catch((e: Error) => {
this.logger.error(e)
})
d.readable.push(body.fixml)
}
}
}
private subscribe (): void {
const router = this.router
const app = this.config.description.application
const root = app?.http?.uri
const authorise = `${root}authorise`
const query = `${root}query`
this.logger.info(`uri: authorise ${authorise}, query ${query}`)
router.post(authorise, async (req: express.Request, res: express.Response) => {
if (!req.headers.authorization) {
this.logger.info('logon')
await this.logon(req, res)
} else {
this.logger.info('logout')
await this.logout(req, res)
}
})
router.get(query, async (req: express.Request, res: express.Response) => {
const headers = req.headers
const body: IFixmlRequest = req.body
const t: MsgTransport | null = this.keys.get(headers.authorization ?? '') ?? null
if (!t) {
this.logger.info('received request with no token')
res.send({
error: 'no key with query'
})
} else {
const d = t.duplex
this.respond(d, res).then(() => {
this.logger.info(`responded to ${req.url}`)
}).catch(e => {
res.send(e)
})
d.readable.push(body.fixml)
}
})
}
}