jspurefix 5.9.1 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKPORT_PLAN.md +20 -7
- package/README.md +35 -0
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-encoder.d.ts +1 -0
- package/dist/buffer/ascii/ascii-encoder.js +15 -0
- package/dist/buffer/ascii/ascii-encoder.js.map +1 -1
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-encoder.d.ts +2 -0
- package/dist/buffer/msg-encoder.js +1 -0
- package/dist/buffer/msg-encoder.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/runtime/session-container.d.ts +3 -0
- package/dist/runtime/session-container.js +8 -0
- package/dist/runtime/session-container.js.map +1 -1
- package/dist/runtime/session-launcher.d.ts +2 -1
- package/dist/runtime/session-launcher.js +4 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/ascii/ascii-msg-transmitter.d.ts +1 -0
- package/dist/transport/ascii/ascii-msg-transmitter.js +15 -0
- package/dist/transport/ascii/ascii-msg-transmitter.js.map +1 -1
- package/dist/transport/ascii/ascii-session-msg-factory.js +1 -7
- package/dist/transport/ascii/ascii-session-msg-factory.js.map +1 -1
- package/dist/transport/fixml/fixml-session-msg-factory.js +1 -6
- package/dist/transport/fixml/fixml-session-msg-factory.js.map +1 -1
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/msg-payload.d.ts +3 -1
- package/dist/transport/msg-payload.js +2 -1
- package/dist/transport/msg-payload.js.map +1 -1
- package/dist/transport/msg-transmitter.d.ts +4 -1
- package/dist/transport/msg-transmitter.js +18 -2
- package/dist/transport/msg-transmitter.js.map +1 -1
- package/dist/transport/send-callback.d.ts +7 -0
- package/dist/transport/send-callback.js +3 -0
- package/dist/transport/send-callback.js.map +1 -0
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +2 -1
- package/dist/transport/session/fix-session.js +12 -3
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-description.d.ts +2 -0
- package/dist/transport/session/session-description.js.map +1 -1
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/docs/benchmarking.md +117 -0
- package/docs/custom-logon.md +225 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +9 -1
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-encoder.ts +20 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-encoder.ts +12 -0
- package/src/buffer/msg-view.ts +35 -18
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/runtime/session-container.ts +20 -0
- package/src/runtime/session-launcher.ts +19 -2
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/ascii/ascii-msg-transmitter.ts +20 -0
- package/src/transport/ascii/ascii-session-msg-factory.ts +6 -1
- package/src/transport/fixml/fixml-session-msg-factory.ts +3 -1
- package/src/transport/index.ts +5 -0
- package/src/transport/msg-payload.ts +5 -1
- package/src/transport/msg-transmitter.ts +31 -2
- package/src/transport/send-callback.ts +33 -0
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/fix-session.ts +22 -3
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-description.ts +14 -0
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-initiator.ts +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import * as fs from 'fs'
|
|
5
|
+
import minimist = require('minimist')
|
|
6
|
+
import buildOptions from 'minimist-options'
|
|
7
|
+
|
|
8
|
+
import { AsciiChars } from './buffer/ascii'
|
|
9
|
+
import { IJsFixConfig } from './config'
|
|
10
|
+
import { DefinitionFactory } from './util'
|
|
11
|
+
import { SessionContainer } from './runtime'
|
|
12
|
+
import { DITokens } from './runtime/di-tokens'
|
|
13
|
+
import {
|
|
14
|
+
BenchRunner,
|
|
15
|
+
IBenchCase,
|
|
16
|
+
IBenchOptions,
|
|
17
|
+
IBenchSuiteResult,
|
|
18
|
+
ParseCase,
|
|
19
|
+
ParseDepth,
|
|
20
|
+
defaultBenchOptions,
|
|
21
|
+
formatComparison,
|
|
22
|
+
formatSuite
|
|
23
|
+
} from './benchmark'
|
|
24
|
+
|
|
25
|
+
const options = buildOptions({
|
|
26
|
+
dict: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
alias: 'd',
|
|
29
|
+
default: 'qf44'
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
fix: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
alias: 'f',
|
|
35
|
+
default: 'data/examples/FIX.4.4/quickfix/execution-report/fix.txt'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
session: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
alias: 's',
|
|
41
|
+
default: 'data/session/test-initiator.json'
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
delimiter: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
alias: 'l',
|
|
47
|
+
default: '|'
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
depth: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
default: 'view,object'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
rounds: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
default: defaultBenchOptions.rounds
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
msgs: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
default: defaultBenchOptions.operationsPerRound
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
warmup: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
default: defaultBenchOptions.warmupOperations
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
label: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
default: 'jspurefix'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
save: {
|
|
76
|
+
type: 'string'
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
baseline: {
|
|
80
|
+
type: 'string'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
json: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
default: false
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
help: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
alias: 'h',
|
|
91
|
+
default: false
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const argv: any = minimist(process.argv.slice(2), options)
|
|
96
|
+
|
|
97
|
+
const root: string = path.join(__dirname, '../')
|
|
98
|
+
|
|
99
|
+
function norm (p: string): string {
|
|
100
|
+
return path.isAbsolute(p) ? p : path.join(root, p)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function usage (): void {
|
|
104
|
+
console.log('measure the parse path and what it costs in time and in garbage')
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(' --fix=<path> fix log to parse, defaults to a 4.4 execution report')
|
|
107
|
+
console.log(' --dict=<name> dictionary, e.g. qf44, repo44, qf50sp2')
|
|
108
|
+
console.log(' --delimiter=<char> field delimiter in the log, defaults to |')
|
|
109
|
+
console.log(' --depth=view,object which stages to measure, comma separated')
|
|
110
|
+
console.log(' --rounds=<n> measured rounds, one sample each')
|
|
111
|
+
console.log(' --msgs=<n> messages per round')
|
|
112
|
+
console.log(' --warmup=<n> messages discarded before measuring')
|
|
113
|
+
console.log(' --save=<path> write the suite to json')
|
|
114
|
+
console.log(' --baseline=<path> compare against a previously saved suite')
|
|
115
|
+
console.log(' --json print the suite as json instead of a report')
|
|
116
|
+
console.log()
|
|
117
|
+
console.log('run under --expose-gc so rounds can be isolated and heap read cleanly:')
|
|
118
|
+
console.log(' node --expose-gc dist/jsfix-bench --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt')
|
|
119
|
+
console.log(' npm run bench')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function makeConfig (): Promise<IJsFixConfig> {
|
|
123
|
+
const sys = new SessionContainer()
|
|
124
|
+
sys.registerGlobal('error')
|
|
125
|
+
const sessionDescription = require(norm(argv.session))
|
|
126
|
+
const container = await sys.makeSystem(sessionDescription)
|
|
127
|
+
const config = container.resolve<IJsFixConfig>(DITokens.IJsFixConfig)
|
|
128
|
+
config.definitions = await new DefinitionFactory().getDefinitions(argv.dict)
|
|
129
|
+
// the parser reads the delimiter when it is constructed, so this has to be
|
|
130
|
+
// settled before any case is built
|
|
131
|
+
config.delimiter = AsciiChars.firstChar(argv.delimiter)
|
|
132
|
+
return config
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requestedDepths (): ParseDepth[] {
|
|
136
|
+
const known = new Map<string, ParseDepth>([
|
|
137
|
+
['view', ParseDepth.View],
|
|
138
|
+
['object', ParseDepth.Object]
|
|
139
|
+
])
|
|
140
|
+
const asked = String(argv.depth).split(',').map(d => d.trim().toLowerCase()).filter(d => d.length > 0)
|
|
141
|
+
return asked.map(d => {
|
|
142
|
+
const depth = known.get(d)
|
|
143
|
+
if (!depth) {
|
|
144
|
+
throw new Error(`unknown depth ${d}, expected view or object`)
|
|
145
|
+
}
|
|
146
|
+
return depth
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readSuite (p: string): IBenchSuiteResult {
|
|
151
|
+
return JSON.parse(fs.readFileSync(norm(p), 'utf8'))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function main (): Promise<void> {
|
|
155
|
+
if (argv.help) {
|
|
156
|
+
usage()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const fixPath = norm(argv.fix)
|
|
160
|
+
if (!fs.existsSync(fixPath)) {
|
|
161
|
+
throw new Error(`no such fix log ${fixPath}`)
|
|
162
|
+
}
|
|
163
|
+
const contents = fs.readFileSync(fixPath, 'utf8')
|
|
164
|
+
const config = await makeConfig()
|
|
165
|
+
|
|
166
|
+
const cases: ParseCase[] = requestedDepths().map(depth =>
|
|
167
|
+
new ParseCase(`parse:${depth}`, { config, contents, depth }))
|
|
168
|
+
|
|
169
|
+
// every case walks the same input, so one alignment covers the suite and all
|
|
170
|
+
// of them stay directly comparable
|
|
171
|
+
const perInvocation = cases[0].operationsPerInvocation
|
|
172
|
+
const operationsPerRound = cases[0].alignOperations(argv.msgs)
|
|
173
|
+
const benchOptions: IBenchOptions = {
|
|
174
|
+
warmupOperations: cases[0].alignOperations(argv.warmup),
|
|
175
|
+
rounds: argv.rounds,
|
|
176
|
+
operationsPerRound,
|
|
177
|
+
gcBetweenRounds: true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`${path.relative(root, fixPath)} ${perInvocation} msg/pass dict ${argv.dict}`)
|
|
181
|
+
const suite = await new BenchRunner(benchOptions).runAll(argv.label, cases as IBenchCase[])
|
|
182
|
+
|
|
183
|
+
if (argv.json) {
|
|
184
|
+
console.log(JSON.stringify(suite, null, 2))
|
|
185
|
+
} else {
|
|
186
|
+
console.log()
|
|
187
|
+
console.log(formatSuite(suite))
|
|
188
|
+
}
|
|
189
|
+
if (argv.baseline) {
|
|
190
|
+
console.log()
|
|
191
|
+
console.log(formatComparison(readSuite(argv.baseline), suite))
|
|
192
|
+
}
|
|
193
|
+
if (argv.save) {
|
|
194
|
+
const target = norm(argv.save)
|
|
195
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
196
|
+
fs.writeFileSync(target, JSON.stringify(suite, null, 2), 'utf8')
|
|
197
|
+
console.log(`saved ${path.relative(root, target)}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch(e => {
|
|
202
|
+
console.error(e instanceof Error ? e.message : e)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
})
|
|
@@ -20,7 +20,23 @@ import { FixmlEncoder, FiXmlParser } from '../buffer/fixml'
|
|
|
20
20
|
import { FixEntity } from '../transport/fix-entity'
|
|
21
21
|
import { IHttpAdapter } from '../transport/http/http-adapter'
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Supplies the session message factory for a description. Return null to accept the
|
|
25
|
+
* stock factory for the protocol.
|
|
26
|
+
*/
|
|
27
|
+
export type SessionMsgFactoryProvider = (description: ISessionDescription) => ISessionMsgFactory | null
|
|
28
|
+
|
|
23
29
|
export class SessionContainer {
|
|
30
|
+
/**
|
|
31
|
+
* @param msgFactoryProvider an application that needs its own session messages -
|
|
32
|
+
* typically a Logon carrying tags the standard message does not - can pass one
|
|
33
|
+
* here rather than subclassing to override makeSessionFactory:
|
|
34
|
+
*
|
|
35
|
+
* new SessionContainer(d => new MyFactory(d))
|
|
36
|
+
*/
|
|
37
|
+
constructor (public readonly msgFactoryProvider: SessionMsgFactoryProvider | null = null) {
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
public reset (): void {
|
|
25
41
|
container.reset()
|
|
26
42
|
}
|
|
@@ -45,6 +61,10 @@ export class SessionContainer {
|
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
protected makeSessionFactory (description: ISessionDescription): ISessionMsgFactory {
|
|
64
|
+
const supplied = this.msgFactoryProvider?.(description)
|
|
65
|
+
if (supplied) {
|
|
66
|
+
return supplied
|
|
67
|
+
}
|
|
48
68
|
const fixml = !this.isAscii(description)
|
|
49
69
|
return fixml
|
|
50
70
|
? new FixmlSessionMsgFactory(description)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from 'path'
|
|
2
2
|
import { IJsFixConfig, IJsFixLogger, JsFixLoggerFactory, JsFixWinstonLogFactory, WinstonLogger } from '../config'
|
|
3
|
-
import { FixEntity, FixSession, ISessionDescription } from '../transport'
|
|
3
|
+
import { FixEntity, FixSession, ISessionDescription, ISessionMsgFactory } from '../transport'
|
|
4
4
|
import { DependencyContainer } from 'tsyringe'
|
|
5
5
|
import { EngineFactory } from './engine-factory'
|
|
6
6
|
import { SessionContainer } from './session-container'
|
|
@@ -24,7 +24,24 @@ export abstract class SessionLauncher {
|
|
|
24
24
|
this.acceptorConfig = acceptorConfig ? this.loadConfig(acceptorConfig) : null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// the arrow defers to makeSessionMsgFactory at resolve time, so a subclass need
|
|
28
|
+
// only override that one method - no SessionContainer subclass required
|
|
29
|
+
protected sessionContainer: SessionContainer =
|
|
30
|
+
new SessionContainer((d: ISessionDescription) => this.makeSessionMsgFactory(d))
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Override to supply this application's own session message factory - the place to
|
|
34
|
+
* put a Logon carrying tags the standard message does not, or a header a
|
|
35
|
+
* counterparty stamps differently. Return null (the default) for the stock
|
|
36
|
+
* factory.
|
|
37
|
+
*
|
|
38
|
+
* For a Logon that only needs extra fields, prefer the "Logon" block in the session
|
|
39
|
+
* description - it needs no code at all. Reach for a factory when the values are
|
|
40
|
+
* computed at run time (a signature, a nonce, a token fetched at start up).
|
|
41
|
+
*/
|
|
42
|
+
protected makeSessionMsgFactory (_description: ISessionDescription): ISessionMsgFactory | null {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
private async empty (): Promise<any> {
|
|
30
47
|
return await new Promise((resolve, reject) => {
|
|
@@ -22,7 +22,7 @@ export class FileSessionStore implements IFixSessionStore {
|
|
|
22
22
|
private creationTimeValue: Date = new Date()
|
|
23
23
|
|
|
24
24
|
// In-memory index: seqnum -> { offset, length }
|
|
25
|
-
private readonly headerIndex
|
|
25
|
+
private readonly headerIndex = new Map<number, { offset: number, length: number }>()
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Creates a FileSessionStore with the default file-based stream provider.
|
|
@@ -285,7 +285,7 @@ export class FileSessionStore implements IFixSessionStore {
|
|
|
285
285
|
*/
|
|
286
286
|
static parseSessionTime (str: string): Date | null {
|
|
287
287
|
// Format: YYYYMMDD-HH:MM:SS.ffffff
|
|
288
|
-
const match =
|
|
288
|
+
const match = /^(\d{4})(\d{2})(\d{2})-(\d{2}):(\d{2}):(\d{2})\.(\d{3,6})$/.exec(str)
|
|
289
289
|
if (!match) return null
|
|
290
290
|
const [, y, M, d, h, m, s, frac] = match
|
|
291
291
|
const ms = parseInt(frac.substring(0, 3), 10)
|
|
@@ -32,28 +32,34 @@ export class FileSessionStreamProvider implements ISessionStreamProvider {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
openBody (): void {
|
|
35
|
-
|
|
36
|
-
this.bodyFd = fs.openSync(this.bodyPath, 'a+')
|
|
37
|
-
const stat = fs.fstatSync(this.bodyFd)
|
|
38
|
-
this.bodySize = stat.size
|
|
35
|
+
this.openBodyFd()
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
/**
|
|
39
|
+
* open the body file on first use and hand back the descriptor - returning it
|
|
40
|
+
* keeps callers free of a null check the compiler cannot see through.
|
|
41
|
+
*/
|
|
42
|
+
private openBodyFd (): number {
|
|
42
43
|
if (this.bodyFd === null) {
|
|
43
|
-
this.
|
|
44
|
+
this.bodyFd = fs.openSync(this.bodyPath, 'a+')
|
|
45
|
+
const stat = fs.fstatSync(this.bodyFd)
|
|
46
|
+
this.bodySize = stat.size
|
|
44
47
|
}
|
|
48
|
+
return this.bodyFd
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async appendBody (data: Buffer): Promise<number> {
|
|
52
|
+
const fd = this.openBodyFd()
|
|
45
53
|
const offset = this.bodySize
|
|
46
|
-
fs.writeSync(
|
|
54
|
+
fs.writeSync(fd, data, 0, data.length, offset)
|
|
47
55
|
this.bodySize += data.length
|
|
48
56
|
return offset
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
async readBody (offset: number, length: number): Promise<Buffer> {
|
|
52
|
-
|
|
53
|
-
this.openBody()
|
|
54
|
-
}
|
|
60
|
+
const fd = this.openBodyFd()
|
|
55
61
|
const buf = Buffer.alloc(length)
|
|
56
|
-
fs.readSync(
|
|
62
|
+
fs.readSync(fd, buf, 0, length, offset)
|
|
57
63
|
return buf
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -7,7 +7,7 @@ import { FileSessionStore } from './file-session-store'
|
|
|
7
7
|
* Factory for creating session stores.
|
|
8
8
|
*/
|
|
9
9
|
export interface IFixSessionStoreFactory {
|
|
10
|
-
create (sessionId: SessionId)
|
|
10
|
+
create: (sessionId: SessionId) => IFixSessionStore
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -14,24 +14,24 @@ export interface IFixSessionStore {
|
|
|
14
14
|
readonly sessionId: SessionId
|
|
15
15
|
|
|
16
16
|
// Message Operations
|
|
17
|
-
put (record: IFixMsgStoreRecord)
|
|
18
|
-
get (seqNum: number)
|
|
19
|
-
getRange (fromSeqNum: number, toSeqNum: number)
|
|
17
|
+
put: (record: IFixMsgStoreRecord) => Promise<void>
|
|
18
|
+
get: (seqNum: number) => Promise<IFixMsgStoreRecord | null>
|
|
19
|
+
getRange: (fromSeqNum: number, toSeqNum: number) => Promise<IFixMsgStoreRecord[]>
|
|
20
20
|
|
|
21
21
|
// Sequence Number Operations
|
|
22
22
|
senderSeqNum: number
|
|
23
23
|
targetSeqNum: number
|
|
24
|
-
setSenderSeqNum (value: number)
|
|
25
|
-
setTargetSeqNum (value: number)
|
|
26
|
-
nextSenderSeqNum ()
|
|
27
|
-
nextTargetSeqNum ()
|
|
24
|
+
setSenderSeqNum: (value: number) => Promise<void>
|
|
25
|
+
setTargetSeqNum: (value: number) => Promise<void>
|
|
26
|
+
nextSenderSeqNum: () => Promise<number>
|
|
27
|
+
nextTargetSeqNum: () => Promise<number>
|
|
28
28
|
|
|
29
29
|
// Session Operations
|
|
30
30
|
readonly creationTime: Date
|
|
31
|
-
reset ()
|
|
31
|
+
reset: () => Promise<void>
|
|
32
32
|
|
|
33
33
|
// Lifecycle
|
|
34
|
-
initialize ()
|
|
35
|
-
flush ()
|
|
36
|
-
dispose ()
|
|
34
|
+
initialize: () => Promise<void>
|
|
35
|
+
flush: () => Promise<void>
|
|
36
|
+
dispose: () => Promise<void>
|
|
37
37
|
}
|
|
@@ -7,7 +7,7 @@ import { IFixMsgStoreRecord } from './fix-msg-store-record'
|
|
|
7
7
|
* Not persistent - all data lost on dispose.
|
|
8
8
|
*/
|
|
9
9
|
export class MemorySessionStore implements IFixSessionStore {
|
|
10
|
-
private readonly messages
|
|
10
|
+
private readonly messages = new Map<number, IFixMsgStoreRecord>()
|
|
11
11
|
private senderSeqNumValue: number = 1
|
|
12
12
|
private targetSeqNumValue: number = 1
|
|
13
13
|
private creationTimeValue: Date = new Date()
|
|
@@ -7,68 +7,68 @@ export interface ISessionStreamProvider {
|
|
|
7
7
|
* Opens or creates a read-write buffer for the message body file.
|
|
8
8
|
* Must support random-access reads (by offset+length).
|
|
9
9
|
*/
|
|
10
|
-
openBody ()
|
|
10
|
+
openBody: () => void
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Appends data to the body. Returns the offset at which data was written.
|
|
14
14
|
*/
|
|
15
|
-
appendBody (data: Buffer)
|
|
15
|
+
appendBody: (data: Buffer) => Promise<number>
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Reads data from the body at the given offset and length.
|
|
19
19
|
*/
|
|
20
|
-
readBody (offset: number, length: number)
|
|
20
|
+
readBody: (offset: number, length: number) => Promise<Buffer>
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Gets the current body size (for calculating offsets).
|
|
24
24
|
*/
|
|
25
|
-
getBodySize ()
|
|
25
|
+
getBodySize: () => number
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Appends a line to the header index file.
|
|
29
29
|
*/
|
|
30
|
-
appendHeaderLine (line: string)
|
|
30
|
+
appendHeaderLine: (line: string) => Promise<void>
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Reads all lines from the header index file.
|
|
34
34
|
* Returns empty array if no data exists.
|
|
35
35
|
*/
|
|
36
|
-
readHeaderLines ()
|
|
36
|
+
readHeaderLines: () => Promise<string[]>
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Reads the sequence numbers string.
|
|
40
40
|
* Returns null if no data exists.
|
|
41
41
|
*/
|
|
42
|
-
readSeqNums ()
|
|
42
|
+
readSeqNums: () => Promise<string | null>
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Writes the sequence numbers string.
|
|
46
46
|
*/
|
|
47
|
-
writeSeqNums (content: string)
|
|
47
|
+
writeSeqNums: (content: string) => Promise<void>
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Reads the session time string.
|
|
51
51
|
* Returns null if no data exists.
|
|
52
52
|
*/
|
|
53
|
-
readSessionTime ()
|
|
53
|
+
readSessionTime: () => Promise<string | null>
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Writes the session time string.
|
|
57
57
|
*/
|
|
58
|
-
writeSessionTime (content: string)
|
|
58
|
+
writeSessionTime: (content: string) => Promise<void>
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Resets all streams/files for a new session.
|
|
62
62
|
*/
|
|
63
|
-
reset ()
|
|
63
|
+
reset: () => Promise<void>
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Flushes any pending writes.
|
|
67
67
|
*/
|
|
68
|
-
flush ()
|
|
68
|
+
flush: () => Promise<void>
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Disposes of all resources.
|
|
72
72
|
*/
|
|
73
|
-
dispose ()
|
|
73
|
+
dispose: () => Promise<void>
|
|
74
74
|
}
|
|
@@ -28,6 +28,26 @@ export class AsciiMsgTransmitter extends MsgTransmitter {
|
|
|
28
28
|
const components = config.definitions.component
|
|
29
29
|
this.header = components.get('StandardHeader')
|
|
30
30
|
this.trailer = components.get('StandardTrailer')
|
|
31
|
+
this.reportUnknownFields(config)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A field the dictionary does not declare on the message being encoded has no tag,
|
|
36
|
+
* so it cannot be written and is dropped. Say so once per offending field - the
|
|
37
|
+
* alternative, and what this library did for years, is that a bespoke Logon tag
|
|
38
|
+
* simply never appears on the wire and nothing anywhere explains why.
|
|
39
|
+
*/
|
|
40
|
+
private reportUnknownFields (config: IJsFixConfig): void {
|
|
41
|
+
const name = config.description.application?.name ?? 'na'
|
|
42
|
+
const logger = config.logFactory.logger(`${name}:encoder`)
|
|
43
|
+
const reported = new Set<string>()
|
|
44
|
+
this.encoder.onUnknownField = (fieldName: string, setName: string) => {
|
|
45
|
+
const key = `${setName}.${fieldName}`
|
|
46
|
+
if (reported.has(key)) return
|
|
47
|
+
reported.add(key)
|
|
48
|
+
logger.warning(`field '${fieldName}' is not declared on ${setName} in this dictionary - it will not be sent`)
|
|
49
|
+
this.emit('unknown-field', fieldName, setName)
|
|
50
|
+
}
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
private checksum (): number {
|
|
@@ -23,7 +23,12 @@ export class AsciiSessionMsgFactory extends ASessionMsgFactory {
|
|
|
23
23
|
Password: description.Password,
|
|
24
24
|
HeartBtInt: description.HeartBtInt,
|
|
25
25
|
ResetSeqNumFlag: description.ResetSeqNumFlag,
|
|
26
|
-
EncryptMethod: EncryptMethod.None
|
|
26
|
+
EncryptMethod: EncryptMethod.None,
|
|
27
|
+
// a counterparty demanding a tag the standard Logon does not carry needs no
|
|
28
|
+
// code at all - name it under "Logon" in the session description. Spread last
|
|
29
|
+
// so an application can also override what the engine derived, or null out a
|
|
30
|
+
// field it does not want sent. See issue #93.
|
|
31
|
+
...description.Logon
|
|
27
32
|
} as ILogon
|
|
28
33
|
return this.mutate(o, MsgType.Logon)
|
|
29
34
|
}
|
|
@@ -42,7 +42,9 @@ export class FixmlSessionMsgFactory extends ASessionMsgFactory {
|
|
|
42
42
|
Username: description.Username,
|
|
43
43
|
Password: description.Password,
|
|
44
44
|
UserRequestID: userRequestId,
|
|
45
|
-
UserRequestType: UserRequestType.LogOnUser
|
|
45
|
+
UserRequestType: UserRequestType.LogOnUser,
|
|
46
|
+
// same escape hatch as the ascii factory - see ISessionDescription.Logon
|
|
47
|
+
...description.Logon
|
|
46
48
|
} as IUserRequest
|
|
47
49
|
return this.mutate(o, MsgType.Logon)
|
|
48
50
|
} else {
|
package/src/transport/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ export * from './fix-acceptor'
|
|
|
4
4
|
export * from './fix-initiator'
|
|
5
5
|
export * from './msg-transmitter'
|
|
6
6
|
export * from './msg-payload'
|
|
7
|
+
export * from './send-callback'
|
|
7
8
|
export * from './tick-action'
|
|
8
9
|
export * from './msg-application'
|
|
9
10
|
export * from './ascii/ascii-session'
|
|
11
|
+
// the factory an application extends to customise its Logon or header - it belongs
|
|
12
|
+
// at the root alongside ASessionMsgFactory, not behind a dist/ path
|
|
13
|
+
export * from './ascii/ascii-session-msg-factory'
|
|
14
|
+
export * from './fixml/fixml-session-msg-factory'
|
|
10
15
|
export * from './session'
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ILooseObject } from '../collections/collection'
|
|
2
|
+
import { SendCallback } from './send-callback'
|
|
2
3
|
|
|
3
4
|
export class MsgPayload {
|
|
4
5
|
public encoded: Buffer
|
|
5
|
-
constructor (public readonly msgType: string,
|
|
6
|
+
constructor (public readonly msgType: string,
|
|
7
|
+
public readonly obj: ILooseObject,
|
|
8
|
+
/** invoked once this payload has been encoded, or has failed to encode */
|
|
9
|
+
public readonly callback: SendCallback | null = null) {
|
|
6
10
|
}
|
|
7
11
|
}
|
|
@@ -4,6 +4,7 @@ import { Transform } from 'stream'
|
|
|
4
4
|
import { MsgPayload } from './msg-payload'
|
|
5
5
|
import { ILooseObject } from '../collections/collection'
|
|
6
6
|
import { ISessionDescription } from './session/session-description'
|
|
7
|
+
import { SendCallback } from './send-callback'
|
|
7
8
|
import * as events from 'events'
|
|
8
9
|
|
|
9
10
|
export abstract class MsgTransmitter extends events.EventEmitter {
|
|
@@ -24,8 +25,23 @@ export abstract class MsgTransmitter extends events.EventEmitter {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// messages at front, byte stream at back
|
|
27
|
-
public send (msgType: string, obj: ILooseObject): void {
|
|
28
|
-
this.encodeStream.write(new MsgPayload(msgType, obj))
|
|
28
|
+
public send (msgType: string, obj: ILooseObject, callback: SendCallback | null = null): void {
|
|
29
|
+
this.encodeStream.write(new MsgPayload(msgType, obj, callback))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Report a send outcome to whoever asked for one, without letting their code break
|
|
34
|
+
* the encode stream - a throwing callback would otherwise surface as a transform
|
|
35
|
+
* failure and be indistinguishable from an encoding fault.
|
|
36
|
+
*/
|
|
37
|
+
protected settle (payload: MsgPayload, error: Error | null, header: ILooseObject | null): void {
|
|
38
|
+
const callback = payload.callback
|
|
39
|
+
if (!callback) return
|
|
40
|
+
try {
|
|
41
|
+
callback(error, { msgType: payload.msgType, header, encoded: payload.encoded ?? null })
|
|
42
|
+
} catch (e) {
|
|
43
|
+
this.emit('error', e)
|
|
44
|
+
}
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
public abstract encodeMessage (msgType: string, obj: ILooseObject): (ILooseObject | null)
|
|
@@ -46,8 +62,21 @@ export abstract class MsgTransmitter extends events.EventEmitter {
|
|
|
46
62
|
this.push(payload.encoded)
|
|
47
63
|
const encodedTxt = transmitter.buffer.toString()
|
|
48
64
|
transmitter.emit('encoded', msgType, encodedTxt, state)
|
|
65
|
+
// encodeMessage returns null when it could not build the message - an
|
|
66
|
+
// unknown msgType, or a factory that produced no header. It reports that
|
|
67
|
+
// on the error channel and carries on, so without this the callback would
|
|
68
|
+
// announce success for a message that never formed.
|
|
69
|
+
state
|
|
70
|
+
? transmitter.settle(payload, null, state)
|
|
71
|
+
: transmitter.settle(payload, new Error(`could not encode ${msgType}`), null)
|
|
72
|
+
// note: the second argument to a Transform callback is *pushed to the
|
|
73
|
+
// readable side*, and this readable side is piped to the socket - so a
|
|
74
|
+
// result cannot be returned that way without corrupting the byte stream.
|
|
75
|
+
// Hence the per-payload callback above.
|
|
49
76
|
done()
|
|
50
77
|
} catch (e) {
|
|
78
|
+
transmitter.settle(payload, e as Error, null)
|
|
79
|
+
// still fail the stream, so the session's error handling is unchanged
|
|
51
80
|
done(e)
|
|
52
81
|
}
|
|
53
82
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ILooseObject } from '../collections/collection'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a send produced. `header` is the StandardHeader the engine stamped on the
|
|
5
|
+
* message - the sequence number it went out under, the sending time, the comp ids -
|
|
6
|
+
* which is the piece applications most often need to correlate an outbound message
|
|
7
|
+
* with an acknowledgement, a database row, or a resend.
|
|
8
|
+
*/
|
|
9
|
+
export interface SendResult {
|
|
10
|
+
msgType: string
|
|
11
|
+
/** the stamped StandardHeader, or null if the message never got that far */
|
|
12
|
+
header: ILooseObject | null
|
|
13
|
+
/** the bytes written to the transport */
|
|
14
|
+
encoded: Buffer | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Called once per `send`, after the message has been encoded and pushed to the
|
|
19
|
+
* transport, or as soon as encoding fails.
|
|
20
|
+
*
|
|
21
|
+
* `send` is otherwise fire and forget: it writes into a stream and returns, so
|
|
22
|
+
* nothing tells the caller which sequence number their message went out under, and a
|
|
23
|
+
* failure surfaces on the session's error channel with no way to tell which call
|
|
24
|
+
* caused it. See https://github.com/TimelordUK/jspurefix/issues/86
|
|
25
|
+
*
|
|
26
|
+
* Note this fires when the message has been *encoded and handed to the transport*,
|
|
27
|
+
* not when the socket has flushed it - the FIX session layer has no acknowledgement
|
|
28
|
+
* of the latter either.
|
|
29
|
+
*
|
|
30
|
+
* A callback that throws is reported on the transmitter's error channel rather than
|
|
31
|
+
* being allowed to tear down the encode stream.
|
|
32
|
+
*/
|
|
33
|
+
export type SendCallback = (error: Error | null, result: SendResult) => void
|