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.
Files changed (145) hide show
  1. package/BACKPORT_PLAN.md +20 -7
  2. package/README.md +35 -0
  3. package/WORKLOG.md +119 -0
  4. package/dist/benchmark/bench-report.d.ts +5 -0
  5. package/dist/benchmark/bench-report.js +91 -0
  6. package/dist/benchmark/bench-report.js.map +1 -0
  7. package/dist/benchmark/bench-runner.d.ts +12 -0
  8. package/dist/benchmark/bench-runner.js +193 -0
  9. package/dist/benchmark/bench-runner.js.map +1 -0
  10. package/dist/benchmark/bench-stats.d.ts +10 -0
  11. package/dist/benchmark/bench-stats.js +56 -0
  12. package/dist/benchmark/bench-stats.js.map +1 -0
  13. package/dist/benchmark/bench-types.d.ts +74 -0
  14. package/dist/benchmark/bench-types.js +3 -0
  15. package/dist/benchmark/bench-types.js.map +1 -0
  16. package/dist/benchmark/gc-probe.d.ts +18 -0
  17. package/dist/benchmark/gc-probe.js +103 -0
  18. package/dist/benchmark/gc-probe.js.map +1 -0
  19. package/dist/benchmark/index.d.ts +6 -0
  20. package/dist/benchmark/index.js +23 -0
  21. package/dist/benchmark/index.js.map +1 -0
  22. package/dist/benchmark/parse-bench.d.ts +28 -0
  23. package/dist/benchmark/parse-bench.js +85 -0
  24. package/dist/benchmark/parse-bench.js.map +1 -0
  25. package/dist/buffer/ascii/ascii-encoder.d.ts +1 -0
  26. package/dist/buffer/ascii/ascii-encoder.js +15 -0
  27. package/dist/buffer/ascii/ascii-encoder.js.map +1 -1
  28. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  29. package/dist/buffer/ascii/ascii-parser.js +4 -1
  30. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  31. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  32. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  33. package/dist/buffer/ascii/tag-index.js.map +1 -1
  34. package/dist/buffer/msg-encoder.d.ts +2 -0
  35. package/dist/buffer/msg-encoder.js +1 -0
  36. package/dist/buffer/msg-encoder.js.map +1 -1
  37. package/dist/buffer/msg-view.d.ts +1 -0
  38. package/dist/buffer/msg-view.js +25 -19
  39. package/dist/buffer/msg-view.js.map +1 -1
  40. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  41. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  42. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  43. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  44. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  45. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  46. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  47. package/dist/jsfix-bench.d.ts +1 -0
  48. package/dist/jsfix-bench.js +213 -0
  49. package/dist/jsfix-bench.js.map +1 -0
  50. package/dist/runtime/session-container.d.ts +3 -0
  51. package/dist/runtime/session-container.js +8 -0
  52. package/dist/runtime/session-container.js.map +1 -1
  53. package/dist/runtime/session-launcher.d.ts +2 -1
  54. package/dist/runtime/session-launcher.js +4 -1
  55. package/dist/runtime/session-launcher.js.map +1 -1
  56. package/dist/store/file-session-store.js +1 -1
  57. package/dist/store/file-session-store.js.map +1 -1
  58. package/dist/store/file-session-stream-provider.d.ts +1 -0
  59. package/dist/store/file-session-stream-provider.js +13 -13
  60. package/dist/store/file-session-stream-provider.js.map +1 -1
  61. package/dist/store/fix-session-store-factory.d.ts +1 -1
  62. package/dist/store/fix-session-store-factory.js.map +1 -1
  63. package/dist/store/fix-session-store.d.ts +11 -11
  64. package/dist/store/fix-session-store.js.map +1 -1
  65. package/dist/store/memory-session-store.js.map +1 -1
  66. package/dist/store/session-stream-provider.d.ts +13 -13
  67. package/dist/store/session-stream-provider.js.map +1 -1
  68. package/dist/transport/ascii/ascii-msg-transmitter.d.ts +1 -0
  69. package/dist/transport/ascii/ascii-msg-transmitter.js +15 -0
  70. package/dist/transport/ascii/ascii-msg-transmitter.js.map +1 -1
  71. package/dist/transport/ascii/ascii-session-msg-factory.js +1 -7
  72. package/dist/transport/ascii/ascii-session-msg-factory.js.map +1 -1
  73. package/dist/transport/fixml/fixml-session-msg-factory.js +1 -6
  74. package/dist/transport/fixml/fixml-session-msg-factory.js.map +1 -1
  75. package/dist/transport/index.d.ts +3 -0
  76. package/dist/transport/index.js +3 -0
  77. package/dist/transport/index.js.map +1 -1
  78. package/dist/transport/msg-payload.d.ts +3 -1
  79. package/dist/transport/msg-payload.js +2 -1
  80. package/dist/transport/msg-payload.js.map +1 -1
  81. package/dist/transport/msg-transmitter.d.ts +4 -1
  82. package/dist/transport/msg-transmitter.js +18 -2
  83. package/dist/transport/msg-transmitter.js.map +1 -1
  84. package/dist/transport/send-callback.d.ts +7 -0
  85. package/dist/transport/send-callback.js +3 -0
  86. package/dist/transport/send-callback.js.map +1 -0
  87. package/dist/transport/session/fix-clock.d.ts +1 -1
  88. package/dist/transport/session/fix-clock.js.map +1 -1
  89. package/dist/transport/session/fix-session.d.ts +2 -1
  90. package/dist/transport/session/fix-session.js +12 -3
  91. package/dist/transport/session/fix-session.js.map +1 -1
  92. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  93. package/dist/transport/session/resend-request-manager.js.map +1 -1
  94. package/dist/transport/session/session-description.d.ts +2 -0
  95. package/dist/transport/session/session-description.js.map +1 -1
  96. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  97. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  98. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  99. package/dist/transport/session/session-sequence-store.js.map +1 -1
  100. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  101. package/docs/benchmarking.md +117 -0
  102. package/docs/custom-logon.md +225 -0
  103. package/eslint.config.mjs +15 -1
  104. package/jsfix.test_client.txt +62 -62
  105. package/jsfix.test_server.txt +72 -72
  106. package/package.json +9 -1
  107. package/src/benchmark/bench-report.ts +101 -0
  108. package/src/benchmark/bench-runner.ts +196 -0
  109. package/src/benchmark/bench-stats.ts +86 -0
  110. package/src/benchmark/bench-types.ts +110 -0
  111. package/src/benchmark/gc-probe.ts +145 -0
  112. package/src/benchmark/index.ts +6 -0
  113. package/src/benchmark/parse-bench.ts +144 -0
  114. package/src/buffer/ascii/ascii-encoder.ts +20 -0
  115. package/src/buffer/ascii/ascii-parser.ts +16 -1
  116. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  117. package/src/buffer/ascii/tag-index.ts +3 -3
  118. package/src/buffer/msg-encoder.ts +12 -0
  119. package/src/buffer/msg-view.ts +35 -18
  120. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  121. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  122. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  123. package/src/jsfix-bench.ts +204 -0
  124. package/src/runtime/session-container.ts +20 -0
  125. package/src/runtime/session-launcher.ts +19 -2
  126. package/src/store/file-session-store.ts +2 -2
  127. package/src/store/file-session-stream-provider.ts +17 -11
  128. package/src/store/fix-session-store-factory.ts +1 -1
  129. package/src/store/fix-session-store.ts +11 -11
  130. package/src/store/memory-session-store.ts +1 -1
  131. package/src/store/session-stream-provider.ts +13 -13
  132. package/src/transport/ascii/ascii-msg-transmitter.ts +20 -0
  133. package/src/transport/ascii/ascii-session-msg-factory.ts +6 -1
  134. package/src/transport/fixml/fixml-session-msg-factory.ts +3 -1
  135. package/src/transport/index.ts +5 -0
  136. package/src/transport/msg-payload.ts +5 -1
  137. package/src/transport/msg-transmitter.ts +31 -2
  138. package/src/transport/send-callback.ts +33 -0
  139. package/src/transport/session/fix-clock.ts +1 -1
  140. package/src/transport/session/fix-session.ts +22 -3
  141. package/src/transport/session/resend-request-manager.ts +4 -4
  142. package/src/transport/session/session-description.ts +14 -0
  143. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  144. package/src/transport/session/session-sequence-store.ts +3 -3
  145. 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
- protected sessionContainer: SessionContainer = new SessionContainer()
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: Map<number, { offset: number, length: number }> = new Map()
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 = str.match(/^(\d{4})(\d{2})(\d{2})-(\d{2}):(\d{2}):(\d{2})\.(\d{3,6})$/)
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
- if (this.bodyFd !== null) return
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
- async appendBody (data: Buffer): Promise<number> {
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.openBody()
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(this.bodyFd!, data, 0, data.length, offset)
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
- if (this.bodyFd === null) {
53
- this.openBody()
54
- }
60
+ const fd = this.openBodyFd()
55
61
  const buf = Buffer.alloc(length)
56
- fs.readSync(this.bodyFd!, buf, 0, length, offset)
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): IFixSessionStore
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): Promise<void>
18
- get (seqNum: number): Promise<IFixMsgStoreRecord | null>
19
- getRange (fromSeqNum: number, toSeqNum: number): Promise<IFixMsgStoreRecord[]>
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): Promise<void>
25
- setTargetSeqNum (value: number): Promise<void>
26
- nextSenderSeqNum (): Promise<number>
27
- nextTargetSeqNum (): Promise<number>
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 (): Promise<void>
31
+ reset: () => Promise<void>
32
32
 
33
33
  // Lifecycle
34
- initialize (): Promise<void>
35
- flush (): Promise<void>
36
- dispose (): Promise<void>
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: Map<number, IFixMsgStoreRecord> = new Map()
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 (): void
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): Promise<number>
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): Promise<Buffer>
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 (): number
25
+ getBodySize: () => number
26
26
 
27
27
  /**
28
28
  * Appends a line to the header index file.
29
29
  */
30
- appendHeaderLine (line: string): Promise<void>
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 (): Promise<string[]>
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 (): Promise<string | null>
42
+ readSeqNums: () => Promise<string | null>
43
43
 
44
44
  /**
45
45
  * Writes the sequence numbers string.
46
46
  */
47
- writeSeqNums (content: string): Promise<void>
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 (): Promise<string | null>
53
+ readSessionTime: () => Promise<string | null>
54
54
 
55
55
  /**
56
56
  * Writes the session time string.
57
57
  */
58
- writeSessionTime (content: string): Promise<void>
58
+ writeSessionTime: (content: string) => Promise<void>
59
59
 
60
60
  /**
61
61
  * Resets all streams/files for a new session.
62
62
  */
63
- reset (): Promise<void>
63
+ reset: () => Promise<void>
64
64
 
65
65
  /**
66
66
  * Flushes any pending writes.
67
67
  */
68
- flush (): Promise<void>
68
+ flush: () => Promise<void>
69
69
 
70
70
  /**
71
71
  * Disposes of all resources.
72
72
  */
73
- dispose (): Promise<void>
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 {
@@ -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, public readonly obj: ILooseObject) {
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
@@ -1,5 +1,5 @@
1
1
  export interface IFixClock {
2
- now (): Date
2
+ now: () => Date
3
3
  }
4
4
 
5
5
  export class DefaultFixClock implements IFixClock {