jspurefix 5.8.5 → 5.9.2

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 (180) hide show
  1. package/.gitattributes +4 -0
  2. package/BACKPORT_PLAN.md +84 -0
  3. package/DEMO_PORT_PLAN.md +10 -0
  4. package/README.md +3 -0
  5. package/WORKLOG.md +119 -0
  6. package/dist/benchmark/bench-report.d.ts +5 -0
  7. package/dist/benchmark/bench-report.js +91 -0
  8. package/dist/benchmark/bench-report.js.map +1 -0
  9. package/dist/benchmark/bench-runner.d.ts +12 -0
  10. package/dist/benchmark/bench-runner.js +193 -0
  11. package/dist/benchmark/bench-runner.js.map +1 -0
  12. package/dist/benchmark/bench-stats.d.ts +10 -0
  13. package/dist/benchmark/bench-stats.js +56 -0
  14. package/dist/benchmark/bench-stats.js.map +1 -0
  15. package/dist/benchmark/bench-types.d.ts +74 -0
  16. package/dist/benchmark/bench-types.js +3 -0
  17. package/dist/benchmark/bench-types.js.map +1 -0
  18. package/dist/benchmark/gc-probe.d.ts +18 -0
  19. package/dist/benchmark/gc-probe.js +103 -0
  20. package/dist/benchmark/gc-probe.js.map +1 -0
  21. package/dist/benchmark/index.d.ts +6 -0
  22. package/dist/benchmark/index.js +23 -0
  23. package/dist/benchmark/index.js.map +1 -0
  24. package/dist/benchmark/parse-bench.d.ts +28 -0
  25. package/dist/benchmark/parse-bench.js +85 -0
  26. package/dist/benchmark/parse-bench.js.map +1 -0
  27. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  28. package/dist/buffer/ascii/ascii-parser.js +4 -1
  29. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  30. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  31. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  32. package/dist/buffer/ascii/tag-index.js.map +1 -1
  33. package/dist/buffer/msg-view.d.ts +1 -0
  34. package/dist/buffer/msg-view.js +25 -19
  35. package/dist/buffer/msg-view.js.map +1 -1
  36. package/dist/config/js-fix-config.d.ts +4 -0
  37. package/dist/config/js-fix-config.js.map +1 -1
  38. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  39. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  40. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  41. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  42. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  43. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  44. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  45. package/dist/jsfix-bench.d.ts +1 -0
  46. package/dist/jsfix-bench.js +213 -0
  47. package/dist/jsfix-bench.js.map +1 -0
  48. package/dist/runtime/session-launcher.js +1 -1
  49. package/dist/runtime/session-launcher.js.map +1 -1
  50. package/dist/runtime/session-scope.d.ts +3 -0
  51. package/dist/runtime/session-scope.js +47 -0
  52. package/dist/runtime/session-scope.js.map +1 -0
  53. package/dist/sample/http/oms/app.js +3 -3
  54. package/dist/sample/http/oms/app.js.map +1 -1
  55. package/dist/sample/tcp/qf-md/app.js +3 -3
  56. package/dist/sample/tcp/qf-md/app.js.map +1 -1
  57. package/dist/sample/tcp/skeleton/app.js +1 -1
  58. package/dist/sample/tcp/skeleton/app.js.map +1 -1
  59. package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
  60. package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
  61. package/dist/store/file-session-store.js +1 -1
  62. package/dist/store/file-session-store.js.map +1 -1
  63. package/dist/store/file-session-stream-provider.d.ts +1 -0
  64. package/dist/store/file-session-stream-provider.js +13 -13
  65. package/dist/store/file-session-stream-provider.js.map +1 -1
  66. package/dist/store/fix-session-store-factory.d.ts +1 -1
  67. package/dist/store/fix-session-store-factory.js.map +1 -1
  68. package/dist/store/fix-session-store.d.ts +11 -11
  69. package/dist/store/fix-session-store.js.map +1 -1
  70. package/dist/store/memory-session-store.js.map +1 -1
  71. package/dist/store/session-stream-provider.d.ts +13 -13
  72. package/dist/store/session-stream-provider.js.map +1 -1
  73. package/dist/transport/ascii/ascii-session.d.ts +17 -3
  74. package/dist/transport/ascii/ascii-session.js +121 -8
  75. package/dist/transport/ascii/ascii-session.js.map +1 -1
  76. package/dist/transport/duplex/fix-duplex.d.ts +1 -0
  77. package/dist/transport/duplex/fix-duplex.js +3 -0
  78. package/dist/transport/duplex/fix-duplex.js.map +1 -1
  79. package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
  80. package/dist/transport/duplex/tcp-duplex.js +5 -0
  81. package/dist/transport/duplex/tcp-duplex.js.map +1 -1
  82. package/dist/transport/factory/msg-transport.d.ts +2 -0
  83. package/dist/transport/factory/msg-transport.js +17 -2
  84. package/dist/transport/factory/msg-transport.js.map +1 -1
  85. package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
  86. package/dist/transport/session/a-session-msg-factory.js +4 -0
  87. package/dist/transport/session/a-session-msg-factory.js.map +1 -1
  88. package/dist/transport/session/clone-session-description.d.ts +6 -0
  89. package/dist/transport/session/clone-session-description.js +11 -0
  90. package/dist/transport/session/clone-session-description.js.map +1 -0
  91. package/dist/transport/session/fix-clock.d.ts +1 -1
  92. package/dist/transport/session/fix-clock.js.map +1 -1
  93. package/dist/transport/session/fix-session.d.ts +5 -1
  94. package/dist/transport/session/fix-session.js +93 -24
  95. package/dist/transport/session/fix-session.js.map +1 -1
  96. package/dist/transport/session/index.d.ts +2 -0
  97. package/dist/transport/session/index.js +2 -0
  98. package/dist/transport/session/index.js.map +1 -1
  99. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  100. package/dist/transport/session/resend-request-manager.js.map +1 -1
  101. package/dist/transport/session/session-msg-factory.d.ts +1 -0
  102. package/dist/transport/session/session-msg-factory.js.map +1 -1
  103. package/dist/transport/session/session-registry.d.ts +23 -0
  104. package/dist/transport/session/session-registry.js +59 -0
  105. package/dist/transport/session/session-registry.js.map +1 -0
  106. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  107. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  108. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  109. package/dist/transport/session/session-sequence-store.js.map +1 -1
  110. package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
  111. package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
  112. package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
  113. package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
  114. package/dist/transport/tcp/tcp-acceptor.js +74 -20
  115. package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
  116. package/dist/transport/tcp/tcp-initiator.js +15 -5
  117. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  118. package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
  119. package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
  120. package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
  121. package/dist/transport/tcp/tls-diagnostics.js +64 -0
  122. package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
  123. package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
  124. package/dist/transport/tcp/tls-options-factory.js +24 -2
  125. package/dist/transport/tcp/tls-options-factory.js.map +1 -1
  126. package/docs/acceptor.md +207 -0
  127. package/docs/benchmarking.md +117 -0
  128. package/eslint.config.mjs +15 -1
  129. package/jsfix.test_client.txt +62 -75
  130. package/jsfix.test_server.txt +72 -72
  131. package/package.json +19 -10
  132. package/script/pack-local.js +68 -0
  133. package/src/benchmark/bench-report.ts +101 -0
  134. package/src/benchmark/bench-runner.ts +196 -0
  135. package/src/benchmark/bench-stats.ts +86 -0
  136. package/src/benchmark/bench-types.ts +110 -0
  137. package/src/benchmark/gc-probe.ts +145 -0
  138. package/src/benchmark/index.ts +6 -0
  139. package/src/benchmark/parse-bench.ts +144 -0
  140. package/src/buffer/ascii/ascii-parser.ts +16 -1
  141. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  142. package/src/buffer/ascii/tag-index.ts +3 -3
  143. package/src/buffer/msg-view.ts +35 -18
  144. package/src/config/js-fix-config.ts +8 -0
  145. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  146. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  147. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  148. package/src/jsfix-bench.ts +204 -0
  149. package/src/runtime/session-launcher.ts +5 -1
  150. package/src/runtime/session-scope.ts +84 -0
  151. package/src/sample/http/oms/app.ts +3 -3
  152. package/src/sample/tcp/qf-md/app.ts +3 -3
  153. package/src/sample/tcp/skeleton/app.ts +1 -1
  154. package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
  155. package/src/store/file-session-store.ts +2 -2
  156. package/src/store/file-session-stream-provider.ts +17 -11
  157. package/src/store/fix-session-store-factory.ts +1 -1
  158. package/src/store/fix-session-store.ts +11 -11
  159. package/src/store/memory-session-store.ts +1 -1
  160. package/src/store/session-stream-provider.ts +13 -13
  161. package/src/transport/ascii/ascii-session.ts +186 -11
  162. package/src/transport/duplex/fix-duplex.ts +9 -0
  163. package/src/transport/duplex/tcp-duplex.ts +10 -0
  164. package/src/transport/factory/msg-transport.ts +20 -0
  165. package/src/transport/session/a-session-msg-factory.ts +18 -0
  166. package/src/transport/session/clone-session-description.ts +41 -0
  167. package/src/transport/session/fix-clock.ts +1 -1
  168. package/src/transport/session/fix-session.ts +136 -25
  169. package/src/transport/session/index.ts +2 -0
  170. package/src/transport/session/resend-request-manager.ts +4 -4
  171. package/src/transport/session/session-msg-factory.ts +13 -0
  172. package/src/transport/session/session-registry.ts +112 -0
  173. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  174. package/src/transport/session/session-sequence-store.ts +3 -3
  175. package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
  176. package/src/transport/tcp/tcp-acceptor.ts +92 -18
  177. package/src/transport/tcp/tcp-initiator.ts +26 -5
  178. package/src/transport/tcp/tcp-transport-description.ts +7 -0
  179. package/src/transport/tcp/tls-diagnostics.ts +76 -0
  180. package/src/transport/tcp/tls-options-factory.ts +34 -2
@@ -0,0 +1,144 @@
1
+ import { AsciiParser } from '../buffer/ascii/ascii-parser'
2
+ import { ElasticBuffer } from '../buffer/elastic-buffer'
3
+ import { MsgView } from '../buffer/msg-view'
4
+ import { IJsFixConfig } from '../config/js-fix-config'
5
+ import { DITokens } from '../runtime/di-tokens'
6
+ import { makeSessionScope } from '../runtime/session-scope'
7
+ import { IBenchCase } from './bench-types'
8
+
9
+ /**
10
+ * what a decoded message is taken through once the parser has produced a view
11
+ */
12
+ export enum ParseDepth {
13
+ /** stop at the MsgView - tokenise, discover the structure, build the view */
14
+ View = 'view',
15
+ /** materialise the whole object graph, which is what most consumers do */
16
+ Object = 'object'
17
+ }
18
+
19
+ export interface IParseCaseOptions {
20
+ readonly config: IJsFixConfig
21
+ /** the raw text of a fix log, one or more complete messages */
22
+ readonly contents: string
23
+ readonly depth: ParseDepth
24
+ }
25
+
26
+ /**
27
+ * Drives fix bytes through the real parser and counts messages out of it.
28
+ *
29
+ * The bytes are fed as one buffer per invocation, the same way a socket read
30
+ * reaches parse, rather than by concatenating the file thousands of times into
31
+ * one enormous string. That concatenation is itself a large allocation, and
32
+ * doing it before the timer starts leaves a heap that no memory reading taken
33
+ * afterwards can be trusted through.
34
+ */
35
+ export class ParseCase implements IBenchCase {
36
+ public readonly name: string
37
+ public readonly description: string
38
+ /** messages produced by one pass over the input */
39
+ public readonly operationsPerInvocation: number
40
+
41
+ private readonly source: Buffer
42
+ private readonly depth: ParseDepth
43
+ private readonly options: IParseCaseOptions
44
+ private parser: AsciiParser | null = null
45
+ private decoded: number = 0
46
+
47
+ constructor (name: string, options: IParseCaseOptions) {
48
+ this.name = name
49
+ this.options = options
50
+ this.depth = options.depth
51
+ this.source = Buffer.from(options.contents)
52
+ this.description = options.depth === ParseDepth.Object
53
+ ? 'tokenise, discover structure, build view, materialise object'
54
+ : 'tokenise, discover structure, build view'
55
+ this.operationsPerInvocation = this.countMessages()
56
+ if (this.operationsPerInvocation === 0) {
57
+ throw new Error(`${name}: input yielded no messages, check the dictionary and the delimiter`)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Round a requested round size up to a whole number of passes over the input.
63
+ * A pass cannot be stopped part way, so asking for a count that is not a
64
+ * multiple of it would silently measure more work than it reported.
65
+ */
66
+ public alignOperations (requested: number): number {
67
+ const per = this.operationsPerInvocation
68
+ return Math.max(per, Math.ceil(requested / per) * per)
69
+ }
70
+
71
+ public setup (): void {
72
+ this.parser = this.makeParser(true)
73
+ }
74
+
75
+ public teardown (): void {
76
+ this.parser?.removeAllListeners()
77
+ this.parser = null
78
+ }
79
+
80
+ public run (operations: number): number {
81
+ const parser = this.parser
82
+ if (!parser) {
83
+ throw new Error(`${this.name}: run called before setup`)
84
+ }
85
+ const source = this.source
86
+ const passes = operations / this.operationsPerInvocation
87
+ if (!Number.isInteger(passes)) {
88
+ throw new Error(`${this.name}: ${operations} operations is not a whole number of passes over the input`)
89
+ }
90
+ this.decoded = 0
91
+ for (let i = 0; i < passes; ++i) {
92
+ parser.parseBuffer(source)
93
+ }
94
+ return this.decoded
95
+ }
96
+
97
+ /**
98
+ * A parser on a session scope of its own.
99
+ *
100
+ * It has to be a scope rather than a hand built AsciiParser: AsciiParserState
101
+ * takes the ParseBuffer from the container, so a parser handed some other
102
+ * buffer writes bytes the state never reads and the parse walks off into
103
+ * nonsense. The scope re-registers the buffer and hands the same instance to
104
+ * both, which is the same thing an accepted connection gets.
105
+ */
106
+ private makeParser (subscribe: boolean): AsciiParser {
107
+ const scoped = makeSessionScope(this.options.config)
108
+ const container = scoped.sessionContainer
109
+ // constructed rather than resolved: DITokens.readStream has no registration
110
+ // outside a live transport, and there is no stream here - the harness hands
111
+ // the parser its bytes. Everything else comes from the scope, so the buffer
112
+ // below is the same instance AsciiParserState will be injected with.
113
+ const buffer = container.resolve<ElasticBuffer>(DITokens.ParseBuffer)
114
+ const maxMessageLen = container.resolve<number>(DITokens.maxMessageLen)
115
+ const parser = new AsciiParser(scoped, null, buffer, maxMessageLen)
116
+ const object = subscribe && this.depth === ParseDepth.Object
117
+ parser.on('msg', (_: string, view: MsgView) => {
118
+ ++this.decoded
119
+ if (object) {
120
+ // read the result back so the materialisation cannot be treated as
121
+ // dead and folded away
122
+ const materialised = view.toObject()
123
+ if (materialised === null) {
124
+ throw new Error('view produced no object')
125
+ }
126
+ }
127
+ })
128
+ return parser
129
+ }
130
+
131
+ /**
132
+ * How many messages one pass over the input yields. Counted on a throwaway
133
+ * parser during construction so the measured run never has to ask.
134
+ */
135
+ private countMessages (): number {
136
+ const parser = this.makeParser(false)
137
+ this.decoded = 0
138
+ parser.parseBuffer(this.source)
139
+ parser.removeAllListeners()
140
+ const count = this.decoded
141
+ this.decoded = 0
142
+ return count
143
+ }
144
+ }
@@ -39,7 +39,9 @@ export class AsciiParser extends MsgParser {
39
39
  this.id = AsciiParser.nextId++
40
40
  this.segmentParser = config.sessionContainer.resolve<AsciiSegmentParser>(AsciiSegmentParser)
41
41
  this.state = config.sessionContainer.resolve<AsciiParserState>(AsciiParserState)
42
- this.state.locations = new Tags(this.receivingBuffer.size / 10, this.maxMessageLen)
42
+ // floored - Tags hands the starting length to new Array, which rejects a
43
+ // fraction, so any buffer size not divisible by 10 threw from here
44
+ this.state.locations = new Tags(Math.floor(this.receivingBuffer.size / 10), this.maxMessageLen)
43
45
  this.state.definitions = this.config.definitions
44
46
  this.state.beginMessage()
45
47
  if (readStream !== null) {
@@ -101,6 +103,19 @@ export class AsciiParser extends MsgParser {
101
103
  this.parse(buff, buff.length)
102
104
  }
103
105
 
106
+ /**
107
+ * feed bytes as though they had arrived from the transport. This is the path
108
+ * the socket takes - subscribe hands each chunk it receives straight to parse -
109
+ * so a caller driving the parser directly, a benchmark or a replay harness,
110
+ * measures the real thing rather than a copy of it.
111
+ *
112
+ * @param buff bytes to parse
113
+ * @param end how much of buff to consume, all of it by default
114
+ */
115
+ public parseBuffer (buff: Buffer, end: number = buff.length): void {
116
+ this.parse(buff, end)
117
+ }
118
+
104
119
  private parse (readBuffer: Buffer, end: number): void {
105
120
  const state = this.state
106
121
  const eq: number = AsciiChars.Eq
@@ -29,10 +29,13 @@ export class AsciiSegmentParser {
29
29
  public parse (msgType: string, tags: Tags, last: number): Structure | null {
30
30
  // completed segments in that they are fully parsed
31
31
  const segments: SegmentDescription[] = []
32
- const msgDefinition: MessageDefinition | undefined = this.definitions.message.get(msgType)
33
- if (!msgDefinition) {
32
+ const resolved: MessageDefinition | undefined = this.definitions.message.get(msgType)
33
+ if (!resolved) {
34
34
  return null
35
35
  }
36
+ // narrowing of resolved is not visible inside the hoisted helpers below, so
37
+ // carry the definition on a const the compiler already sees as non-optional
38
+ const msgDefinition: MessageDefinition = resolved
36
39
  // in process of being discovered and may have any amount of depth
37
40
  // i.e. a component containing a repeated group of components
38
41
  // with sub-groups of components
@@ -41,8 +44,8 @@ export class AsciiSegmentParser {
41
44
  let peek: SegmentDescription
42
45
 
43
46
  // track depth-1 components that have been exited for fragmentation detection
44
- const exitedDepth1Components: Set<string> = new Set()
45
- const fragmentedComponents: Set<string> = new Set()
47
+ const exitedDepth1Components = new Set<string>()
48
+ const fragmentedComponents = new Set<string>()
46
49
 
47
50
  // having finished one segments keep unwinding until tag matches further up stack
48
51
  function unwind (tag: number): void {
@@ -189,8 +192,8 @@ export class AsciiSegmentParser {
189
192
  // only build SegmentViews for components detected as fragmented during discover
190
193
  // non-fragmented components use their position ranges directly (zero overhead)
191
194
  if (fragmentedComponents.size === 0) return
192
- const ti = new TagIndex(msgDefinition!, tags, last + 1)
193
- const seen: Set<string> = new Set()
195
+ const ti = new TagIndex(msgDefinition, tags, last + 1)
196
+ const seen = new Set<string>()
194
197
  for (let i = 1; i < segments.length - 1; i++) {
195
198
  const seg = segments[i]
196
199
  if (seg.depth !== 1) continue
@@ -10,9 +10,9 @@ export interface TagSpan {
10
10
 
11
11
  export class TagIndex {
12
12
  private readonly sortedTagPos: TagPos[]
13
- private readonly tagSpans: Map<number, TagSpan> = new Map()
14
- private readonly componentGroupWrappers: Set<string> = new Set()
15
- private readonly cache: Map<string, SegmentView | null> = new Map()
13
+ private readonly tagSpans = new Map<number, TagSpan>()
14
+ private readonly componentGroupWrappers = new Set<string>()
15
+ private readonly cache = new Map<string, SegmentView | null>()
16
16
 
17
17
  constructor (
18
18
  public readonly set: IContainedSet,
@@ -263,29 +263,46 @@ export abstract class MsgView {
263
263
  return JSON.stringify(this.toObject(), null, 4)
264
264
  }
265
265
 
266
+ /**
267
+ * fetch a nested view, either by a single name - getView('Instrument') - or by a
268
+ * dotted path walked a step at a time - getView('Parties.PtysSubGrp')
269
+ */
266
270
  public getView (name: string): MsgView | null {
267
- const parts: string[] = name.split('.')
268
- const reducer = (a: MsgView | null, current: string): MsgView | null => {
269
- if (!a) {
270
- return a
271
+ // the dotted form is the exception. resolve a plain name without paying for
272
+ // the parts array and the closure a split and reduce would allocate - the
273
+ // component walk in toObject and validate comes through here per component.
274
+ if (!name.includes('.')) {
275
+ return this.resolveChild(name)
276
+ }
277
+ let view: MsgView | null = this
278
+ for (const part of name.split('.')) {
279
+ view = view.resolveChild(part)
280
+ if (!view) {
281
+ return null
271
282
  }
272
- const structure = a.structure
273
- const singleton: SegmentDescription | null = structure?.firstContainedWithin(current, a.segment) ?? null
274
- if (singleton) {
275
- return a.create(singleton)
276
- }
277
- // is this a full name where abbreviation exists
278
- const component: ContainedField | null = a.segment.set?.localNameToField.get(current) ?? null
283
+ }
284
+ return view
285
+ }
279
286
 
280
- if (component) {
281
- const abbreviated: SegmentDescription | null = structure?.firstContainedWithin(component.name, a.segment) ?? null
282
- if (abbreviated) {
283
- return a.create(abbreviated)
284
- }
287
+ /**
288
+ * resolve one step of a view path against this view
289
+ */
290
+ private resolveChild (name: string): MsgView | null {
291
+ const structure = this.structure
292
+ const singleton: SegmentDescription | null = structure?.firstContainedWithin(name, this.segment) ?? null
293
+ if (singleton) {
294
+ return this.create(singleton)
295
+ }
296
+ // is this a full name where abbreviation exists
297
+ const component: ContainedField | null = this.segment.set?.localNameToField.get(name) ?? null
298
+
299
+ if (component) {
300
+ const abbreviated: SegmentDescription | null = structure?.firstContainedWithin(component.name, this.segment) ?? null
301
+ if (abbreviated) {
302
+ return this.create(abbreviated)
285
303
  }
286
- return null
287
304
  }
288
- return parts.reduce<MsgView | null>(reducer, this)
305
+ return null
289
306
  }
290
307
 
291
308
  public abstract checksum (): number
@@ -6,6 +6,7 @@ import { EmptyLogFactory } from './empty-log-factory'
6
6
  import { AsciiChars } from '../buffer/ascii/ascii-chars'
7
7
  import { DependencyContainer } from 'tsyringe'
8
8
  import { IFixSessionStoreFactory } from '../store/fix-session-store-factory'
9
+ import { ISessionRegistry } from '../transport/session/session-registry'
9
10
 
10
11
  export interface IJsFixConfig {
11
12
  factory: ISessionMsgFactory | null
@@ -16,11 +17,18 @@ export interface IJsFixConfig {
16
17
  logFactory: JsFixLoggerFactory
17
18
  sessionContainer: DependencyContainer
18
19
  sessionStoreFactory?: IFixSessionStoreFactory
20
+ /**
21
+ * Tracks the live session per SessionId. Set by the acceptor listener so a
22
+ * reconnecting counterparty replaces - rather than duplicates - its session.
23
+ */
24
+ sessionRegistry?: ISessionRegistry
19
25
  }
20
26
 
21
27
  export class JsFixConfig implements IJsFixConfig {
22
28
  public logDelimiter: number = AsciiChars.Pipe
23
29
  public sessionContainer: DependencyContainer
30
+ public sessionStoreFactory?: IFixSessionStoreFactory
31
+ public sessionRegistry?: ISessionRegistry
24
32
  constructor (
25
33
  public readonly factory: ISessionMsgFactory | null,
26
34
  public readonly definitions: FixDefinitions,
@@ -49,7 +49,7 @@ export class DictionaryValidator {
49
49
  private readonly allFieldNames: string[] = []
50
50
  private readonly allComponentNames: string[] = []
51
51
 
52
- get errors (): ReadonlyArray<ValidationError> {
52
+ get errors (): readonly ValidationError[] {
53
53
  return this._errors
54
54
  }
55
55
 
@@ -51,7 +51,7 @@ export class GraphNode {
51
51
  public readonly element: XNode
52
52
  ) {}
53
53
 
54
- get edges (): ReadonlyArray<Edge> {
54
+ get edges (): readonly Edge[] {
55
55
  return this._edges
56
56
  }
57
57
 
@@ -118,8 +118,7 @@ export class QuickFixGraphParser {
118
118
  this.parseTrailer(doc)
119
119
  this.parseMessages(doc)
120
120
 
121
- while (this.queue.length > 0) {
122
- const node = this.queue.shift()!
121
+ for (let node = this.queue.shift(); node !== undefined; node = this.queue.shift()) {
123
122
  this.work(node)
124
123
  }
125
124
 
@@ -14,7 +14,7 @@ export interface ValidationError {
14
14
  }
15
15
 
16
16
  export class DictionaryValidationException extends Error {
17
- constructor (public readonly errors: ReadonlyArray<ValidationError>) {
17
+ constructor (public readonly errors: readonly ValidationError[]) {
18
18
  const errorCount = errors.filter(e => e.severity === ValidationSeverity.Error).length
19
19
  const warningCount = errors.filter(e => e.severity === ValidationSeverity.Warning).length
20
20
  const header = `FIX dictionary validation failed with ${errorCount} error(s) and ${warningCount} warning(s):`
@@ -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
+ })
@@ -126,8 +126,12 @@ export abstract class SessionLauncher {
126
126
  this.registerApplication(container)
127
127
  } else {
128
128
  if (factory.makeSession) {
129
+ // resolve the config from the container doing the resolving, not the one
130
+ // captured here. An acceptor resolves each session from a per-connection
131
+ // child scope which re-registers IJsFixConfig, so this is how an application
132
+ // receives that session's own description, store and message factory.
129
133
  container.register<FixSession>(DITokens.FixSession, {
130
- useFactory: () => factory.makeSession(config)
134
+ useFactory: (c) => factory.makeSession(c.resolve<IJsFixConfig>(DITokens.IJsFixConfig))
131
135
  })
132
136
  }
133
137
  }
@@ -0,0 +1,84 @@
1
+ import { DependencyContainer } from 'tsyringe'
2
+ import { IJsFixConfig, JsFixConfig } from '../config'
3
+ import { DITokens } from './di-tokens'
4
+ import { ElasticBuffer } from '../buffer/elastic-buffer'
5
+ import { ISessionDescription } from '../transport/session/session-description'
6
+ import { ISessionMsgFactory } from '../transport/session/session-msg-factory'
7
+ import { cloneSessionDescription } from '../transport/session/clone-session-description'
8
+ import { AsciiSessionMsgFactory } from '../transport/ascii/ascii-session-msg-factory'
9
+ import { FixmlSessionMsgFactory } from '../transport/fixml/fixml-session-msg-factory'
10
+
11
+ /**
12
+ * A FIX acceptor serves many connections from one listener configuration. Until
13
+ * this existed every accepted connection shared the *same* parse buffer, transmit
14
+ * buffer, session description and message factory, because those are registered as
15
+ * instances on the single session container built at start up. Two clients talking
16
+ * to one acceptor would therefore interleave bytes in a shared ElasticBuffer and
17
+ * compute the same SessionId - so the same message store file.
18
+ *
19
+ * A session scope is a tsyringe child container which re-registers exactly the
20
+ * per-connection state. Everything else (dictionary, transient parser/transmitter
21
+ * classes, logger factory) is inherited from the parent, and resolving a parent
22
+ * registration from the child still injects the child's instances, so an
23
+ * AsciiParser resolved here receives this scope's ParseBuffer.
24
+ *
25
+ * Mirrors cspurefix TradeCaptureSessionFactory.CreatePerSessionConfigAndEncoder,
26
+ * which had to do the same thing for the same reason.
27
+ */
28
+ export function makeSessionScope (
29
+ config: IJsFixConfig,
30
+ overrides?: Partial<ISessionDescription>): IJsFixConfig {
31
+ const parent: DependencyContainer = config.sessionContainer
32
+ const scope = parent.createChildContainer()
33
+
34
+ const description = cloneSessionDescription(config.description, overrides)
35
+ const factory = makeSessionMsgFactory(description, config.factory)
36
+
37
+ scope.registerInstance(DITokens.ISessionDescription, description)
38
+ scope.registerInstance(DITokens.ISessionMsgFactory, factory)
39
+ scope.registerInstance<ElasticBuffer>(DITokens.TransmitBuffer, cloneBuffer(parent, DITokens.TransmitBuffer))
40
+ scope.registerInstance<ElasticBuffer>(DITokens.ParseBuffer, cloneBuffer(parent, DITokens.ParseBuffer))
41
+
42
+ const scoped = new JsFixConfig(factory, config.definitions, description, config.delimiter, config.logFactory)
43
+ scoped.logDelimiter = config.logDelimiter ?? scoped.logDelimiter
44
+ scoped.sessionStoreFactory = config.sessionStoreFactory
45
+ scoped.sessionRegistry = config.sessionRegistry
46
+ scoped.sessionContainer = scope
47
+ scope.registerInstance(DITokens.IJsFixConfig, scoped)
48
+ scope.registerInstance(DITokens.Definitions, config.definitions)
49
+
50
+ return scoped
51
+ }
52
+
53
+ /**
54
+ * A fresh buffer of the same capacity as the one the parent container holds, so the
55
+ * sizing decisions made in SessionContainer.asAscii are honoured without repeating
56
+ * them here.
57
+ */
58
+ function cloneBuffer (parent: DependencyContainer, token: DITokens): ElasticBuffer {
59
+ if (!parent.isRegistered(token, true)) {
60
+ return new ElasticBuffer()
61
+ }
62
+ const existing = parent.resolve<ElasticBuffer>(token)
63
+ return new ElasticBuffer(existing.size)
64
+ }
65
+
66
+ /**
67
+ * The scope needs a factory bound to *its* description, because the outbound header
68
+ * carries this session's comp ids. Applications may supply their own factory (by
69
+ * overriding SessionContainer.makeSessionFactory), and that must survive into the
70
+ * per-session scope - building a stock one here would silently drop custom header
71
+ * handling on the acceptor path. See issue #87.
72
+ */
73
+ function makeSessionMsgFactory (
74
+ description: ISessionDescription,
75
+ current: ISessionMsgFactory | null): ISessionMsgFactory {
76
+ if (current?.cloneFor) {
77
+ return current.cloneFor(description)
78
+ }
79
+ const ascii = description.application?.protocol === 'ascii'
80
+ const mutator = (current as any)?.mutator ?? null
81
+ return ascii
82
+ ? new AsciiSessionMsgFactory(description, mutator)
83
+ : new FixmlSessionMsgFactory(description, mutator)
84
+ }
@@ -19,9 +19,9 @@ class AppLauncher extends SessionLauncher {
19
19
  protected override makeFactory (config: IJsFixConfig): EngineFactory {
20
20
  const isInitiator = this.isInitiator(config.description)
21
21
  return {
22
- makeSession: () => isInitiator
23
- ? new HttpClient(config)
24
- : new HttpServer(config)
22
+ makeSession: (sessionConfig: IJsFixConfig) => isInitiator
23
+ ? new HttpClient(sessionConfig)
24
+ : new HttpServer(sessionConfig)
25
25
  } as EngineFactory
26
26
  }
27
27