jspurefix 5.2.0 → 5.5.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 (92) hide show
  1. package/BACKPORT_PLAN.md +135 -39
  2. package/dist/config/js-fix-config.d.ts +2 -0
  3. package/dist/config/js-fix-config.js.map +1 -1
  4. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +39 -0
  5. package/dist/dictionary/parser/quickfix/dictionary-validator.js +321 -0
  6. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -0
  7. package/dist/dictionary/parser/quickfix/index-visitor.d.ts +10 -0
  8. package/dist/dictionary/parser/quickfix/index-visitor.js +68 -0
  9. package/dist/dictionary/parser/quickfix/index-visitor.js.map +1 -0
  10. package/dist/dictionary/parser/quickfix/index.d.ts +7 -0
  11. package/dist/dictionary/parser/quickfix/index.js +7 -0
  12. package/dist/dictionary/parser/quickfix/index.js.map +1 -1
  13. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.d.ts +13 -0
  14. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js +65 -0
  15. package/dist/dictionary/parser/quickfix/quick-fix-graph-file-parser.js.map +1 -0
  16. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +73 -0
  17. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +363 -0
  18. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -0
  19. package/dist/dictionary/parser/quickfix/sax-tree-builder.d.ts +5 -0
  20. package/dist/dictionary/parser/quickfix/sax-tree-builder.js +103 -0
  21. package/dist/dictionary/parser/quickfix/sax-tree-builder.js.map +1 -0
  22. package/dist/dictionary/parser/quickfix/validation-error.d.ts +17 -0
  23. package/dist/dictionary/parser/quickfix/validation-error.js +32 -0
  24. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -0
  25. package/dist/dictionary/parser/quickfix/x-element.d.ts +26 -0
  26. package/dist/dictionary/parser/quickfix/x-element.js +82 -0
  27. package/dist/dictionary/parser/quickfix/x-element.js.map +1 -0
  28. package/dist/store/file-session-store.d.ts +42 -0
  29. package/dist/store/file-session-store.js +256 -0
  30. package/dist/store/file-session-store.js.map +1 -0
  31. package/dist/store/file-session-stream-provider.d.ts +25 -0
  32. package/dist/store/file-session-stream-provider.js +162 -0
  33. package/dist/store/file-session-stream-provider.js.map +1 -0
  34. package/dist/store/fix-msg-ascii-store-resend.js +6 -0
  35. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  36. package/dist/store/fix-session-store-factory.d.ts +13 -0
  37. package/dist/store/fix-session-store-factory.js +21 -0
  38. package/dist/store/fix-session-store-factory.js.map +1 -0
  39. package/dist/store/fix-session-store.d.ts +19 -0
  40. package/dist/store/fix-session-store.js +3 -0
  41. package/dist/store/fix-session-store.js.map +1 -0
  42. package/dist/store/index.d.ts +9 -0
  43. package/dist/store/index.js +9 -0
  44. package/dist/store/index.js.map +1 -1
  45. package/dist/store/memory-session-store.d.ts +27 -0
  46. package/dist/store/memory-session-store.js +104 -0
  47. package/dist/store/memory-session-store.js.map +1 -0
  48. package/dist/store/memory-session-stream-provider.d.ts +26 -0
  49. package/dist/store/memory-session-stream-provider.js +103 -0
  50. package/dist/store/memory-session-stream-provider.js.map +1 -0
  51. package/dist/store/session-id.d.ts +9 -0
  52. package/dist/store/session-id.js +55 -0
  53. package/dist/store/session-id.js.map +1 -0
  54. package/dist/store/session-stream-provider.d.ts +15 -0
  55. package/dist/store/session-stream-provider.js +3 -0
  56. package/dist/store/session-stream-provider.js.map +1 -0
  57. package/dist/store/store-config.d.ts +5 -0
  58. package/dist/store/store-config.js +3 -0
  59. package/dist/store/store-config.js.map +1 -0
  60. package/dist/transport/ascii/ascii-session.d.ts +6 -1
  61. package/dist/transport/ascii/ascii-session.js +37 -5
  62. package/dist/transport/ascii/ascii-session.js.map +1 -1
  63. package/dist/transport/session/session-description.d.ts +2 -0
  64. package/dist/transport/session/session-description.js.map +1 -1
  65. package/dist/util/definition-factory.js +1 -1
  66. package/dist/util/definition-factory.js.map +1 -1
  67. package/jsfix.test_client.txt +67 -67
  68. package/jsfix.test_server.txt +64 -64
  69. package/package.json +6 -6
  70. package/src/config/js-fix-config.ts +2 -0
  71. package/src/dictionary/parser/quickfix/dictionary-validator.ts +473 -0
  72. package/src/dictionary/parser/quickfix/index-visitor.ts +100 -0
  73. package/src/dictionary/parser/quickfix/index.ts +7 -0
  74. package/src/dictionary/parser/quickfix/quick-fix-graph-file-parser.ts +63 -0
  75. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +450 -0
  76. package/src/dictionary/parser/quickfix/sax-tree-builder.ts +112 -0
  77. package/src/dictionary/parser/quickfix/validation-error.ts +34 -0
  78. package/src/dictionary/parser/quickfix/x-element.ts +115 -0
  79. package/src/store/file-session-store.ts +294 -0
  80. package/src/store/file-session-stream-provider.ts +123 -0
  81. package/src/store/fix-msg-ascii-store-resend.ts +8 -0
  82. package/src/store/fix-session-store-factory.ts +31 -0
  83. package/src/store/fix-session-store.ts +37 -0
  84. package/src/store/index.ts +9 -0
  85. package/src/store/memory-session-store.ts +102 -0
  86. package/src/store/memory-session-stream-provider.ts +97 -0
  87. package/src/store/session-id.ts +32 -0
  88. package/src/store/session-stream-provider.ts +74 -0
  89. package/src/store/store-config.ts +16 -0
  90. package/src/transport/ascii/ascii-session.ts +57 -6
  91. package/src/transport/session/session-description.ts +2 -0
  92. package/src/util/definition-factory.ts +2 -2
@@ -0,0 +1,473 @@
1
+ import { XDocument, XNode } from './x-element'
2
+ import {
3
+ DictionaryValidationException,
4
+ ValidationError,
5
+ ValidationSeverity
6
+ } from './validation-error'
7
+
8
+ interface FieldDefinitionInfo {
9
+ readonly name: string
10
+ readonly tag: number
11
+ readonly type: string
12
+ readonly lineNumber?: number
13
+ }
14
+
15
+ interface ComponentDefinitionInfo {
16
+ readonly name: string
17
+ readonly lineNumber?: number
18
+ }
19
+
20
+ interface MessageDefinitionInfo {
21
+ readonly name: string
22
+ readonly msgType: string
23
+ readonly lineNumber?: number
24
+ }
25
+
26
+ /**
27
+ * Validates FIX dictionary XML for common errors like duplicates, missing references, etc.
28
+ * Port of C# DictionaryValidator — operates on an XDocument tree built by SaxTreeBuilder.
29
+ */
30
+ export class DictionaryValidator {
31
+ private readonly _errors: ValidationError[] = []
32
+
33
+ // Track definitions — case-sensitive for exact matching
34
+ private readonly fieldsByName = new Map<string, FieldDefinitionInfo>()
35
+ private readonly fieldsByTag = new Map<number, FieldDefinitionInfo>()
36
+ private readonly componentsByName = new Map<string, ComponentDefinitionInfo>()
37
+ private readonly messagesByName = new Map<string, MessageDefinitionInfo>()
38
+ private readonly messagesByMsgType = new Map<string, MessageDefinitionInfo>()
39
+
40
+ // Case-insensitive lookup for "did you mean" suggestions
41
+ private readonly fieldNamesCaseInsensitive = new Map<string, string>()
42
+ private readonly componentNamesCaseInsensitive = new Map<string, string>()
43
+
44
+ // Track what's referenced (to find unused definitions)
45
+ private readonly referencedFields = new Set<string>()
46
+ private readonly referencedComponents = new Set<string>()
47
+
48
+ // All known names for "did you mean" suggestions
49
+ private readonly allFieldNames: string[] = []
50
+ private readonly allComponentNames: string[] = []
51
+
52
+ get errors (): ReadonlyArray<ValidationError> {
53
+ return this._errors
54
+ }
55
+
56
+ get hasErrors (): boolean {
57
+ return this._errors.some(e => e.severity === ValidationSeverity.Error)
58
+ }
59
+
60
+ get hasWarnings (): boolean {
61
+ return this._errors.some(e => e.severity === ValidationSeverity.Warning)
62
+ }
63
+
64
+ validate (doc: XDocument): void {
65
+ // First pass: collect all definitions
66
+ this.collectFieldDefinitions(doc)
67
+ this.collectComponentDefinitions(doc)
68
+ this.collectMessageDefinitions(doc)
69
+
70
+ // Second pass: validate references
71
+ this.validateHeader(doc)
72
+ this.validateTrailer(doc)
73
+ this.validateComponentReferences(doc)
74
+ this.validateMessageReferences(doc)
75
+
76
+ // Third pass: check for unused definitions (warnings only)
77
+ this.checkUnusedDefinitions()
78
+ }
79
+
80
+ throwIfErrors (): void {
81
+ if (this.hasErrors) {
82
+ throw new DictionaryValidationException(this._errors)
83
+ }
84
+ }
85
+
86
+ // ── Field Validation ──
87
+
88
+ private collectFieldDefinitions (doc: XDocument): void {
89
+ const fieldsNode = doc.firstDescendant('fields')
90
+ if (!fieldsNode) {
91
+ this.addError('MISSING_FIELDS', 'No <fields> section found in dictionary')
92
+ return
93
+ }
94
+
95
+ for (const field of fieldsNode.elements('field')) {
96
+ this.validateFieldDefinition(field)
97
+ }
98
+ }
99
+
100
+ private validateFieldDefinition (field: XNode): void {
101
+ const name = field.attribute('name')
102
+ const numberStr = field.attribute('number')
103
+ const type = field.attribute('type')
104
+ const lineNumber = field.line
105
+
106
+ if (!name) {
107
+ this.addError('FIELD_NO_NAME', 'Field definition missing \'name\' attribute', undefined, undefined, lineNumber)
108
+ return
109
+ }
110
+
111
+ const tag = numberStr != null ? parseInt(numberStr, 10) : NaN
112
+ if (!numberStr || isNaN(tag)) {
113
+ this.addError('FIELD_NO_TAG', `Field '${name}' missing or invalid 'number' attribute`, name, 'field', lineNumber)
114
+ return
115
+ }
116
+
117
+ if (!type) {
118
+ this.addWarning('FIELD_NO_TYPE', `Field '${name}' missing 'type' attribute, defaulting to STRING`, name, 'field', lineNumber)
119
+ }
120
+
121
+ this.allFieldNames.push(name)
122
+
123
+ // Check for duplicate by name
124
+ const existingByName = this.fieldsByName.get(name)
125
+ if (existingByName) {
126
+ this.addError('DUPLICATE_FIELD_NAME',
127
+ `Duplicate field name '${name}' (tag ${tag}). Previously defined with tag ${existingByName.tag}`,
128
+ name, 'field', lineNumber,
129
+ existingByName.tag === tag ? 'Remove the duplicate definition' : 'Use unique field names')
130
+ return
131
+ }
132
+
133
+ // Check for duplicate by tag
134
+ const existingByTag = this.fieldsByTag.get(tag)
135
+ if (existingByTag) {
136
+ this.addError('DUPLICATE_FIELD_TAG',
137
+ `Duplicate field tag ${tag} for '${name}'. Tag already used by field '${existingByTag.name}'`,
138
+ name, 'field', lineNumber,
139
+ 'Each tag number must be unique')
140
+ return
141
+ }
142
+
143
+ const info: FieldDefinitionInfo = { name, tag, type: type ?? 'STRING', lineNumber }
144
+ this.fieldsByName.set(name, info)
145
+ this.fieldsByTag.set(tag, info)
146
+ this.fieldNamesCaseInsensitive.set(name.toLowerCase(), name)
147
+
148
+ this.validateFieldEnums(field, name, lineNumber)
149
+ }
150
+
151
+ private validateFieldEnums (field: XNode, fieldName: string, lineNumber?: number): void {
152
+ const values = field.elements('value')
153
+ if (values.length === 0) return
154
+
155
+ const seenEnumKeys = new Set<string>()
156
+ const seenEnumDescriptions = new Set<string>()
157
+
158
+ for (const value of values) {
159
+ const enumKey = value.attribute('enum')
160
+ const description = value.attribute('description')
161
+ const valueLine = value.line ?? lineNumber
162
+
163
+ if (!enumKey) {
164
+ this.addError('ENUM_NO_KEY', `Field '${fieldName}' has enum value without 'enum' attribute`,
165
+ fieldName, 'field', valueLine)
166
+ continue
167
+ }
168
+
169
+ if (!description) {
170
+ this.addWarning('ENUM_NO_DESC', `Field '${fieldName}' enum '${enumKey}' has no description`,
171
+ fieldName, 'field', valueLine)
172
+ }
173
+
174
+ if (seenEnumKeys.has(enumKey)) {
175
+ this.addError('DUPLICATE_ENUM_KEY',
176
+ `Field '${fieldName}' has duplicate enum key '${enumKey}'`,
177
+ fieldName, 'field', valueLine)
178
+ }
179
+ seenEnumKeys.add(enumKey)
180
+
181
+ if (description != null) {
182
+ const descLower = description.toLowerCase()
183
+ if (seenEnumDescriptions.has(descLower)) {
184
+ this.addWarning('DUPLICATE_ENUM_DESC',
185
+ `Field '${fieldName}' has duplicate enum description '${description}' which may cause naming conflicts`,
186
+ fieldName, 'field', valueLine)
187
+ }
188
+ seenEnumDescriptions.add(descLower)
189
+ }
190
+ }
191
+ }
192
+
193
+ // ── Component Validation ──
194
+
195
+ private collectComponentDefinitions (doc: XDocument): void {
196
+ const componentsNode = doc.firstDescendant('components')
197
+ if (!componentsNode) {
198
+ // Components are optional
199
+ return
200
+ }
201
+
202
+ for (const component of componentsNode.elements('component')) {
203
+ this.validateComponentDefinition(component)
204
+ }
205
+ }
206
+
207
+ private validateComponentDefinition (component: XNode): void {
208
+ const name = component.attribute('name')
209
+ const lineNumber = component.line
210
+
211
+ if (!name) {
212
+ this.addError('COMPONENT_NO_NAME', 'Component definition missing \'name\' attribute', undefined, undefined, lineNumber)
213
+ return
214
+ }
215
+
216
+ this.allComponentNames.push(name)
217
+
218
+ const existing = this.componentsByName.get(name)
219
+ if (existing) {
220
+ this.addError('DUPLICATE_COMPONENT',
221
+ `Duplicate component name '${name}'`,
222
+ name, 'component', lineNumber,
223
+ `Previously defined at line ${existing.lineNumber ?? '?'}`)
224
+ return
225
+ }
226
+
227
+ this.componentsByName.set(name, { name, lineNumber })
228
+ this.componentNamesCaseInsensitive.set(name.toLowerCase(), name)
229
+
230
+ this.validateFieldReferences(component, name, 'component')
231
+ }
232
+
233
+ // ── Message Validation ──
234
+
235
+ private collectMessageDefinitions (doc: XDocument): void {
236
+ const messagesNode = doc.firstDescendant('messages')
237
+ if (!messagesNode) {
238
+ this.addError('MISSING_MESSAGES', 'No <messages> section found in dictionary')
239
+ return
240
+ }
241
+
242
+ for (const message of messagesNode.elements('message')) {
243
+ this.validateMessageDefinition(message)
244
+ }
245
+ }
246
+
247
+ private validateMessageDefinition (message: XNode): void {
248
+ const name = message.attribute('name')
249
+ const msgType = message.attribute('msgtype')
250
+ const lineNumber = message.line
251
+
252
+ if (!name) {
253
+ this.addError('MESSAGE_NO_NAME', 'Message definition missing \'name\' attribute', undefined, undefined, lineNumber)
254
+ return
255
+ }
256
+
257
+ if (!msgType) {
258
+ this.addError('MESSAGE_NO_MSGTYPE', `Message '${name}' missing 'msgtype' attribute`,
259
+ name, 'message', lineNumber)
260
+ return
261
+ }
262
+
263
+ const existingByName = this.messagesByName.get(name)
264
+ if (existingByName) {
265
+ this.addError('DUPLICATE_MESSAGE_NAME',
266
+ `Duplicate message name '${name}'`,
267
+ name, 'message', lineNumber,
268
+ `Previously defined at line ${existingByName.lineNumber ?? '?'}`)
269
+ return
270
+ }
271
+
272
+ const existingByType = this.messagesByMsgType.get(msgType)
273
+ if (existingByType) {
274
+ this.addError('DUPLICATE_MESSAGE_TYPE',
275
+ `Duplicate message type '${msgType}' for message '${name}'. Type already used by '${existingByType.name}'`,
276
+ name, 'message', lineNumber)
277
+ return
278
+ }
279
+
280
+ const info: MessageDefinitionInfo = { name, msgType, lineNumber }
281
+ this.messagesByName.set(name, info)
282
+ this.messagesByMsgType.set(msgType, info)
283
+
284
+ this.validateFieldReferences(message, name, 'message')
285
+ }
286
+
287
+ // ── Reference Validation ──
288
+
289
+ private validateHeader (doc: XDocument): void {
290
+ const header = doc.firstDescendant('header')
291
+ if (!header) {
292
+ this.addError('MISSING_HEADER', 'No <header> section found in dictionary')
293
+ return
294
+ }
295
+ this.validateFieldReferences(header, 'StandardHeader', 'header')
296
+ }
297
+
298
+ private validateTrailer (doc: XDocument): void {
299
+ const trailer = doc.firstDescendant('trailer')
300
+ if (!trailer) {
301
+ this.addError('MISSING_TRAILER', 'No <trailer> section found in dictionary')
302
+ return
303
+ }
304
+ this.validateFieldReferences(trailer, 'StandardTrailer', 'trailer')
305
+ }
306
+
307
+ private validateComponentReferences (doc: XDocument): void {
308
+ const componentsNode = doc.firstDescendant('components')
309
+ if (!componentsNode) return
310
+
311
+ for (const component of componentsNode.elements('component')) {
312
+ const name = component.attribute('name')
313
+ if (!name) continue
314
+
315
+ for (const compRef of component.descendants('component')) {
316
+ this.validateComponentReference(compRef, name, 'component')
317
+ }
318
+ }
319
+ }
320
+
321
+ private validateMessageReferences (doc: XDocument): void {
322
+ const messagesNode = doc.firstDescendant('messages')
323
+ if (!messagesNode) return
324
+
325
+ for (const message of messagesNode.elements('message')) {
326
+ const name = message.attribute('name')
327
+ if (!name) continue
328
+
329
+ for (const compRef of message.descendants('component')) {
330
+ this.validateComponentReference(compRef, name, 'message')
331
+ }
332
+ }
333
+ }
334
+
335
+ private validateFieldReferences (container: XNode, containerName: string, containerType: string): void {
336
+ for (const fieldRef of container.elements('field')) {
337
+ const fieldName = fieldRef.attribute('name')
338
+ const lineNumber = fieldRef.line
339
+
340
+ if (!fieldName) {
341
+ this.addError('FIELD_REF_NO_NAME',
342
+ `Field reference in ${containerType} '${containerName}' missing 'name' attribute`,
343
+ containerName, containerType, lineNumber)
344
+ continue
345
+ }
346
+
347
+ this.referencedFields.add(fieldName)
348
+
349
+ if (!this.fieldsByName.has(fieldName)) {
350
+ // Check for case mismatch first
351
+ const correctCase = this.fieldNamesCaseInsensitive.get(fieldName.toLowerCase())
352
+ const suggestion = correctCase ?? DictionaryValidator.findSimilar(fieldName, this.allFieldNames)
353
+
354
+ this.addError('UNDEFINED_FIELD',
355
+ `Field '${fieldName}' referenced in ${containerType} '${containerName}' is not defined`,
356
+ fieldName, 'field reference', lineNumber,
357
+ suggestion != null ? `Did you mean '${suggestion}'?` : 'Add the field to the <fields> section')
358
+ }
359
+ }
360
+
361
+ // Recursively check groups
362
+ for (const group of container.elements('group')) {
363
+ const groupName = group.attribute('name') ?? 'unknown'
364
+ this.referencedFields.add(groupName)
365
+
366
+ if (groupName !== 'unknown' && !this.fieldsByName.has(groupName)) {
367
+ const lineNumber = group.line
368
+ const suggestion = DictionaryValidator.findSimilar(groupName, this.allFieldNames)
369
+
370
+ this.addError('UNDEFINED_GROUP_FIELD',
371
+ `Group '${groupName}' in ${containerType} '${containerName}' has no corresponding field definition (for the repeating count)`,
372
+ groupName, 'group', lineNumber,
373
+ suggestion != null ? `Did you mean '${suggestion}'?` : 'Add a NUMINGROUP field for this group')
374
+ }
375
+
376
+ this.validateFieldReferences(group, `${containerName}.${groupName}`, 'group')
377
+ }
378
+ }
379
+
380
+ private validateComponentReference (compRef: XNode, containerName: string, containerType: string): void {
381
+ const compName = compRef.attribute('name')
382
+ const lineNumber = compRef.line
383
+
384
+ if (!compName) {
385
+ this.addError('COMPONENT_REF_NO_NAME',
386
+ `Component reference in ${containerType} '${containerName}' missing 'name' attribute`,
387
+ containerName, containerType, lineNumber)
388
+ return
389
+ }
390
+
391
+ this.referencedComponents.add(compName)
392
+
393
+ if (!this.componentsByName.has(compName) &&
394
+ compName !== 'StandardHeader' && compName !== 'StandardTrailer') {
395
+ const suggestion = DictionaryValidator.findSimilar(compName, this.allComponentNames)
396
+ this.addError('UNDEFINED_COMPONENT',
397
+ `Component '${compName}' referenced in ${containerType} '${containerName}' is not defined`,
398
+ compName, 'component reference', lineNumber,
399
+ suggestion != null ? `Did you mean '${suggestion}'?` : 'Add the component to the <components> section')
400
+ }
401
+ }
402
+
403
+ private checkUnusedDefinitions (): void {
404
+ for (const field of this.fieldsByName.values()) {
405
+ if (!this.referencedFields.has(field.name)) {
406
+ this.addWarning('UNUSED_FIELD',
407
+ `Field '${field.name}' (tag ${field.tag}) is defined but never referenced`,
408
+ field.name, 'field', field.lineNumber)
409
+ }
410
+ }
411
+
412
+ for (const comp of this.componentsByName.values()) {
413
+ if (!this.referencedComponents.has(comp.name)) {
414
+ this.addWarning('UNUSED_COMPONENT',
415
+ `Component '${comp.name}' is defined but never referenced`,
416
+ comp.name, 'component', comp.lineNumber)
417
+ }
418
+ }
419
+ }
420
+
421
+ // ── Helpers ──
422
+
423
+ private addError (code: string, message: string, elementName?: string,
424
+ elementType?: string, lineNumber?: number, suggestion?: string): void {
425
+ this._errors.push({ severity: ValidationSeverity.Error, code, message, elementName, elementType, lineNumber, suggestion })
426
+ }
427
+
428
+ private addWarning (code: string, message: string, elementName?: string,
429
+ elementType?: string, lineNumber?: number, suggestion?: string): void {
430
+ this._errors.push({ severity: ValidationSeverity.Warning, code, message, elementName, elementType, lineNumber, suggestion })
431
+ }
432
+
433
+ static findSimilar (input: string, candidates: string[]): string | null {
434
+ let bestMatch: string | null = null
435
+ let bestDistance = Infinity
436
+ const maxDistance = Math.max(3, Math.floor(input.length / 2))
437
+ const inputLower = input.toLowerCase()
438
+
439
+ for (const candidate of candidates) {
440
+ const distance = DictionaryValidator.levenshteinDistance(inputLower, candidate.toLowerCase())
441
+ if (distance < bestDistance && distance <= maxDistance) {
442
+ bestDistance = distance
443
+ bestMatch = candidate
444
+ }
445
+ }
446
+
447
+ return bestMatch
448
+ }
449
+
450
+ static levenshteinDistance (s1: string, s2: string): number {
451
+ const n = s1.length
452
+ const m = s2.length
453
+ if (n === 0) return m
454
+ if (m === 0) return n
455
+
456
+ const d: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0))
457
+
458
+ for (let i = 0; i <= n; i++) d[i][0] = i
459
+ for (let j = 0; j <= m; j++) d[0][j] = j
460
+
461
+ for (let i = 1; i <= n; i++) {
462
+ for (let j = 1; j <= m; j++) {
463
+ const cost = s1[i - 1] === s2[j - 1] ? 0 : 1
464
+ d[i][j] = Math.min(
465
+ d[i - 1][j] + 1,
466
+ d[i][j - 1] + 1,
467
+ d[i - 1][j - 1] + cost)
468
+ }
469
+ }
470
+
471
+ return d[n][m]
472
+ }
473
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Post-processing reindexer for ContainedFieldSet.
3
+ *
4
+ * After the graph parser drains its work queue, parent sets may not yet
5
+ * know about all tags in their nested groups/components — because those
6
+ * children may have been resolved AFTER the parent. This visitor walks
7
+ * every message/component/group definition in post-order (children first),
8
+ * clears the aggregated tag indices, and re-adds direct fields via
9
+ * ContainedSetBuilder, which propagates child tags upward correctly.
10
+ *
11
+ * Equivalent to the C# IndexVisitor + ContainedFieldSet.Index() pair.
12
+ */
13
+ import { ContainedFieldSet } from '../../contained/contained-field-set'
14
+ import { ContainedField } from '../../contained/contained-field'
15
+ import { ContainedFieldType } from '../../contained/contained-field-type'
16
+ import { ContainedGroupField } from '../../contained/contained-group-field'
17
+ import { ContainedComponentField } from '../../contained/contained-component-field'
18
+ import { ContainedSetBuilder } from '../../contained/contained-set-builder'
19
+ import { FixDefinitions } from '../../definition/fix-definitions'
20
+ import { IContainedSet } from '../../contained/contained-set'
21
+
22
+ export class IndexVisitor {
23
+ private readonly visited = new Set<IContainedSet>()
24
+
25
+ /**
26
+ * Reindex every message in the definitions. Components and groups are
27
+ * reindexed transitively as they are encountered during message reindexing.
28
+ */
29
+ compute (definitions: FixDefinitions): void {
30
+ // Use a Set to dedupe — definitions.message contains duplicate entries (by name + msgType + abbreviation)
31
+ const messages = new Set(definitions.message.values())
32
+ for (const msg of messages) {
33
+ this.reindex(msg)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Reindex a single set: post-order traversal ensures every nested
39
+ * group/component is fully indexed before its parent is rebuilt.
40
+ */
41
+ reindex (set: IContainedSet): void {
42
+ if (this.visited.has(set)) return
43
+ this.visited.add(set)
44
+
45
+ // First, recurse into all child groups/components (post-order)
46
+ for (const field of set.fields) {
47
+ if (field.type === ContainedFieldType.Group) {
48
+ const gf = field as ContainedGroupField
49
+ if (gf.definition) this.reindex(gf.definition)
50
+ } else if (field.type === ContainedFieldType.Component) {
51
+ const cf = field as ContainedComponentField
52
+ if (cf.definition) this.reindex(cf.definition)
53
+ }
54
+ }
55
+
56
+ // Save direct fields, clear all aggregated state, re-add via builder
57
+ const level0: ContainedField[] = [...set.fields]
58
+ IndexVisitor.clearAggregated(set as ContainedFieldSet)
59
+
60
+ const builder = new ContainedSetBuilder(set)
61
+ for (const field of level0) {
62
+ builder.add(field)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Reset every aggregated index on a set without touching its identity.
68
+ * Direct fields are passed back via the return of clearAggregated for
69
+ * the caller to re-add.
70
+ */
71
+ static clearAggregated (set: ContainedFieldSet): void {
72
+ set.fields.length = 0
73
+ set.simple.clear()
74
+ set.groups.clear()
75
+ set.components.clear()
76
+ set.localNameToField.clear()
77
+ set.flattenedTag.length = 0
78
+ set.localAttribute.length = 0
79
+ set.nameToLocalAttribute.clear()
80
+ set.firstSimple = null
81
+ set.containsRaw = false
82
+ IndexVisitor.clearObject(set.containedTag)
83
+ IndexVisitor.clearObject(set.localTag)
84
+ IndexVisitor.clearObject(set.localRequired)
85
+ IndexVisitor.clearObject(set.tagToSimple)
86
+ IndexVisitor.clearObject(set.tagToField)
87
+ IndexVisitor.clearObject(set.containedLength)
88
+ // Re-add the noOfField tag for groups (otherwise the group counter is lost)
89
+ const def = set as any
90
+ if (def.noOfField) {
91
+ set.containedTag[def.noOfField.tag] = true
92
+ }
93
+ }
94
+
95
+ private static clearObject (obj: Record<string, unknown>): void {
96
+ for (const k of Object.keys(obj)) {
97
+ delete obj[k]
98
+ }
99
+ }
100
+ }
@@ -1 +1,8 @@
1
1
  export * from './quick-fix-xml-file-parser'
2
+ export * from './quick-fix-graph-parser'
3
+ export * from './quick-fix-graph-file-parser'
4
+ export * from './x-element'
5
+ export * from './sax-tree-builder'
6
+ export * from './dictionary-validator'
7
+ export * from './validation-error'
8
+ export * from './index-visitor'
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Drop-in replacement for QuickFixXmlFileParser that uses the graph-based
3
+ * parser internally. Maintains the same constructor signature so it can be
4
+ * swapped in the DefinitionFactory without changes to call sites.
5
+ */
6
+ import { FixParser } from '../../fix-parser'
7
+ import { FixDefinitions } from '../../definition/fix-definitions'
8
+ import { GetJsFixLogger } from '../../../config'
9
+ import { MakeDuplex } from '../../../transport'
10
+ import { SaxTreeBuilder } from './sax-tree-builder'
11
+ import { QuickFixGraphParser, QuickFixGraphParserOptions } from './quick-fix-graph-parser'
12
+ import { FixDefinitionSource } from '../../fix-definition-source'
13
+ import { VersionUtil } from '../../version-util'
14
+
15
+ export class QuickFixGraphFileParser extends FixParser {
16
+ constructor (
17
+ public readonly make: MakeDuplex,
18
+ public readonly getLogger: GetJsFixLogger,
19
+ public readonly options: QuickFixGraphParserOptions = {}
20
+ ) {
21
+ super()
22
+ }
23
+
24
+ public async parse (): Promise<FixDefinitions> {
25
+ const xml = await this.readAll()
26
+ const doc = SaxTreeBuilder.parse(xml)
27
+
28
+ const fixRoot = doc.firstDescendant('fix')
29
+ if (!fixRoot) throw new Error('no <fix> root element')
30
+ const major = parseInt(fixRoot.attribute('major') ?? '0', 10)
31
+ const minor = parseInt(fixRoot.attribute('minor') ?? '0', 10)
32
+ const servicepack = parseInt(fixRoot.attribute('servicepack') ?? '0', 10)
33
+ const description = (major !== 5 || servicepack === 0)
34
+ ? `FIX.${major}.${minor}`
35
+ : `FIX.${major}.${minor}SP${servicepack}`
36
+
37
+ const definitions = new FixDefinitions(FixDefinitionSource.QuickFix, VersionUtil.resolve(description))
38
+ const parser = new QuickFixGraphParser(definitions, this.options)
39
+ return parser.parseDocument(doc)
40
+ }
41
+
42
+ /**
43
+ * Read the entire duplex stream into a UTF-8 string. The graph parser
44
+ * needs the full document in memory because it walks the XDocument tree
45
+ * with random access — unlike the legacy SAX-streaming parser.
46
+ */
47
+ private async readAll (): Promise<string> {
48
+ const duplex = this.make()
49
+ const readable = duplex.readable
50
+ return await new Promise<string>((resolve, reject) => {
51
+ const chunks: Buffer[] = []
52
+ readable.on('data', (chunk: Buffer | string) => {
53
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
54
+ })
55
+ readable.on('end', () => {
56
+ resolve(Buffer.concat(chunks).toString('utf-8'))
57
+ })
58
+ readable.on('error', (err: Error) => {
59
+ reject(err)
60
+ })
61
+ })
62
+ }
63
+ }