jspurefix 5.1.0 → 5.3.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 (159) hide show
  1. package/BACKPORT_PLAN.md +138 -79
  2. package/dist/buffer/fixml/fixml-view.js.map +1 -1
  3. package/dist/buffer/msg-encoder.js +34 -1
  4. package/dist/buffer/msg-encoder.js.map +1 -1
  5. package/dist/buffer/msg-parser.js +34 -1
  6. package/dist/buffer/msg-parser.js.map +1 -1
  7. package/dist/buffer/msg-view.js.map +1 -1
  8. package/dist/collections/index.js +1 -0
  9. package/dist/config/js-fix-config.d.ts +2 -0
  10. package/dist/config/js-fix-config.js.map +1 -1
  11. package/dist/config/winston-logger.js.map +1 -1
  12. package/dist/dict-parser.js +34 -1
  13. package/dist/dict-parser.js.map +1 -1
  14. package/dist/dictionary/compiler/enum-compiler.js +37 -4
  15. package/dist/dictionary/compiler/enum-compiler.js.map +1 -1
  16. package/dist/dictionary/compiler/msg-compiler.js +36 -3
  17. package/dist/dictionary/compiler/msg-compiler.js.map +1 -1
  18. package/dist/dictionary/compiler/standard-snippet.js +34 -1
  19. package/dist/dictionary/compiler/standard-snippet.js.map +1 -1
  20. package/dist/dictionary/contained/contained-field-set.js +2 -0
  21. package/dist/dictionary/contained/contained-field-set.js.map +1 -1
  22. package/dist/dictionary/definition/simple-field-definition.js +34 -1
  23. package/dist/dictionary/definition/simple-field-definition.js.map +1 -1
  24. package/dist/dictionary/fix-parser.js +34 -1
  25. package/dist/dictionary/fix-parser.js.map +1 -1
  26. package/dist/dictionary/parser/fix-repository/repository-type.js +1 -0
  27. package/dist/dictionary/parser/fix-repository/repository-xml-parser.js +35 -2
  28. package/dist/dictionary/parser/fix-repository/repository-xml-parser.js.map +1 -1
  29. package/dist/dictionary/parser/fixml/fields-parser.js.map +1 -1
  30. package/dist/dictionary/parser/fixml/fix-xsd-parser.js +34 -1
  31. package/dist/dictionary/parser/fixml/fix-xsd-parser.js.map +1 -1
  32. package/dist/dictionary/parser/fixml/include-graph.js +35 -2
  33. package/dist/dictionary/parser/fixml/include-graph.js.map +1 -1
  34. package/dist/dictionary/parser/fixml/node-definitions.js +1 -0
  35. package/dist/dictionary/parser/fixml/xsd-parser.js +34 -1
  36. package/dist/dictionary/parser/fixml/xsd-parser.js.map +1 -1
  37. package/dist/jsfix-cmd.js +39 -3
  38. package/dist/jsfix-cmd.js.map +1 -1
  39. package/dist/runtime/session-launcher.js +34 -1
  40. package/dist/runtime/session-launcher.js.map +1 -1
  41. package/dist/sample/http/oms/app.js +34 -1
  42. package/dist/sample/http/oms/app.js.map +1 -1
  43. package/dist/sample/tcp/recovering-skeleton/app.js +34 -1
  44. package/dist/sample/tcp/recovering-skeleton/app.js.map +1 -1
  45. package/dist/store/file-session-store.d.ts +42 -0
  46. package/dist/store/file-session-store.js +256 -0
  47. package/dist/store/file-session-store.js.map +1 -0
  48. package/dist/store/file-session-stream-provider.d.ts +25 -0
  49. package/dist/store/file-session-stream-provider.js +162 -0
  50. package/dist/store/file-session-stream-provider.js.map +1 -0
  51. package/dist/store/fix-msg-ascii-store-resend.js +1 -1
  52. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  53. package/dist/store/fix-session-store-factory.d.ts +13 -0
  54. package/dist/store/fix-session-store-factory.js +21 -0
  55. package/dist/store/fix-session-store-factory.js.map +1 -0
  56. package/dist/store/fix-session-store.d.ts +19 -0
  57. package/dist/store/fix-session-store.js +3 -0
  58. package/dist/store/fix-session-store.js.map +1 -0
  59. package/dist/store/index.d.ts +9 -0
  60. package/dist/store/index.js +9 -0
  61. package/dist/store/index.js.map +1 -1
  62. package/dist/store/memory-session-store.d.ts +27 -0
  63. package/dist/store/memory-session-store.js +104 -0
  64. package/dist/store/memory-session-store.js.map +1 -0
  65. package/dist/store/memory-session-stream-provider.d.ts +26 -0
  66. package/dist/store/memory-session-stream-provider.js +103 -0
  67. package/dist/store/memory-session-stream-provider.js.map +1 -0
  68. package/dist/store/session-id.d.ts +9 -0
  69. package/dist/store/session-id.js +55 -0
  70. package/dist/store/session-id.js.map +1 -0
  71. package/dist/store/session-stream-provider.d.ts +15 -0
  72. package/dist/store/session-stream-provider.js +3 -0
  73. package/dist/store/session-stream-provider.js.map +1 -0
  74. package/dist/store/store-config.d.ts +4 -0
  75. package/dist/store/store-config.js +3 -0
  76. package/dist/store/store-config.js.map +1 -0
  77. package/dist/transport/ascii/ascii-session.d.ts +12 -1
  78. package/dist/transport/ascii/ascii-session.js +154 -5
  79. package/dist/transport/ascii/ascii-session.js.map +1 -1
  80. package/dist/transport/duplex/http-duplex.js +4 -1
  81. package/dist/transport/duplex/http-duplex.js.map +1 -1
  82. package/dist/transport/duplex/tcp-duplex.js +34 -1
  83. package/dist/transport/duplex/tcp-duplex.js.map +1 -1
  84. package/dist/transport/fix-acceptor.js +34 -1
  85. package/dist/transport/fix-acceptor.js.map +1 -1
  86. package/dist/transport/fix-entity.js +34 -1
  87. package/dist/transport/fix-entity.js.map +1 -1
  88. package/dist/transport/fixml/fixml-msg-transmitter.js +1 -1
  89. package/dist/transport/fixml/fixml-msg-transmitter.js.map +1 -1
  90. package/dist/transport/http/http-acceptor.js +34 -1
  91. package/dist/transport/http/http-acceptor.js.map +1 -1
  92. package/dist/transport/msg-transmitter.js +34 -1
  93. package/dist/transport/msg-transmitter.js.map +1 -1
  94. package/dist/transport/session/a-session-msg-factory.d.ts +1 -1
  95. package/dist/transport/session/a-session-msg-factory.js.map +1 -1
  96. package/dist/transport/session/fix-clock.d.ts +6 -0
  97. package/dist/transport/session/fix-clock.js +10 -0
  98. package/dist/transport/session/fix-clock.js.map +1 -0
  99. package/dist/transport/session/fix-session.d.ts +1 -0
  100. package/dist/transport/session/fix-session.js +37 -1
  101. package/dist/transport/session/fix-session.js.map +1 -1
  102. package/dist/transport/session/index.d.ts +4 -0
  103. package/dist/transport/session/index.js +4 -0
  104. package/dist/transport/session/index.js.map +1 -1
  105. package/dist/transport/session/resend-request-manager.d.ts +69 -0
  106. package/dist/transport/session/resend-request-manager.js +208 -0
  107. package/dist/transport/session/resend-request-manager.js.map +1 -0
  108. package/dist/transport/session/session-description.d.ts +2 -0
  109. package/dist/transport/session/session-description.js.map +1 -1
  110. package/dist/transport/session/session-msg-factory.d.ts +1 -1
  111. package/dist/transport/session/session-msg-factory.js.map +1 -1
  112. package/dist/transport/session/session-sequence-coordinator.d.ts +38 -0
  113. package/dist/transport/session/session-sequence-coordinator.js +180 -0
  114. package/dist/transport/session/session-sequence-coordinator.js.map +1 -0
  115. package/dist/transport/session/session-sequence-store.d.ts +14 -0
  116. package/dist/transport/session/session-sequence-store.js +36 -0
  117. package/dist/transport/session/session-sequence-store.js.map +1 -0
  118. package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
  119. package/dist/transport/tcp/tcp-initiator.js +34 -1
  120. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  121. package/dist/types/FIX4.4/index.js +1 -0
  122. package/dist/util/buffer-helper.js +34 -1
  123. package/dist/util/buffer-helper.js.map +1 -1
  124. package/dist/util/definition-factory.js +35 -2
  125. package/dist/util/definition-factory.js.map +1 -1
  126. package/jsfix.test_client.txt +67 -66
  127. package/jsfix.test_server.txt +64 -63
  128. package/package.json +11 -10
  129. package/src/buffer/fixml/fixml-view.ts +1 -1
  130. package/src/buffer/msg-view.ts +1 -1
  131. package/src/config/js-fix-config.ts +2 -0
  132. package/src/config/winston-logger.ts +3 -3
  133. package/src/dictionary/contained/contained-field-set.ts +2 -1
  134. package/src/dictionary/parser/fixml/fields-parser.ts +2 -2
  135. package/src/jsfix-cmd.ts +1 -1
  136. package/src/store/file-session-store.ts +294 -0
  137. package/src/store/file-session-stream-provider.ts +123 -0
  138. package/src/store/fix-msg-ascii-store-resend.ts +1 -1
  139. package/src/store/fix-session-store-factory.ts +31 -0
  140. package/src/store/fix-session-store.ts +37 -0
  141. package/src/store/index.ts +9 -0
  142. package/src/store/memory-session-store.ts +102 -0
  143. package/src/store/memory-session-stream-provider.ts +97 -0
  144. package/src/store/session-id.ts +32 -0
  145. package/src/store/session-stream-provider.ts +74 -0
  146. package/src/store/store-config.ts +15 -0
  147. package/src/transport/ascii/ascii-session.ts +218 -6
  148. package/src/transport/fixml/fixml-msg-transmitter.ts +1 -1
  149. package/src/transport/http/http-acceptor.ts +1 -1
  150. package/src/transport/session/a-session-msg-factory.ts +1 -1
  151. package/src/transport/session/fix-clock.ts +9 -0
  152. package/src/transport/session/fix-session.ts +5 -0
  153. package/src/transport/session/index.ts +4 -0
  154. package/src/transport/session/resend-request-manager.ts +268 -0
  155. package/src/transport/session/session-description.ts +2 -0
  156. package/src/transport/session/session-msg-factory.ts +1 -1
  157. package/src/transport/session/session-sequence-coordinator.ts +272 -0
  158. package/src/transport/session/session-sequence-store.ts +33 -0
  159. package/src/transport/tcp/tcp-acceptor.ts +2 -2
@@ -0,0 +1,102 @@
1
+ import { IFixSessionStore } from './fix-session-store'
2
+ import { SessionId } from './session-id'
3
+ import { IFixMsgStoreRecord } from './fix-msg-store-record'
4
+
5
+ /**
6
+ * In-memory session store for testing and development.
7
+ * Not persistent - all data lost on dispose.
8
+ */
9
+ export class MemorySessionStore implements IFixSessionStore {
10
+ private readonly messages: Map<number, IFixMsgStoreRecord> = new Map()
11
+ private senderSeqNumValue: number = 1
12
+ private targetSeqNumValue: number = 1
13
+ private creationTimeValue: Date = new Date()
14
+
15
+ constructor (public readonly sessionId: SessionId) {}
16
+
17
+ // Sequence Numbers
18
+
19
+ get senderSeqNum (): number {
20
+ return this.senderSeqNumValue
21
+ }
22
+
23
+ set senderSeqNum (value: number) {
24
+ this.senderSeqNumValue = value
25
+ }
26
+
27
+ get targetSeqNum (): number {
28
+ return this.targetSeqNumValue
29
+ }
30
+
31
+ set targetSeqNum (value: number) {
32
+ this.targetSeqNumValue = value
33
+ }
34
+
35
+ async setSenderSeqNum (value: number): Promise<void> {
36
+ this.senderSeqNumValue = value
37
+ }
38
+
39
+ async setTargetSeqNum (value: number): Promise<void> {
40
+ this.targetSeqNumValue = value
41
+ }
42
+
43
+ async nextSenderSeqNum (): Promise<number> {
44
+ return ++this.senderSeqNumValue
45
+ }
46
+
47
+ async nextTargetSeqNum (): Promise<number> {
48
+ return ++this.targetSeqNumValue
49
+ }
50
+
51
+ // Session
52
+
53
+ get creationTime (): Date {
54
+ return this.creationTimeValue
55
+ }
56
+
57
+ async reset (): Promise<void> {
58
+ this.senderSeqNumValue = 1
59
+ this.targetSeqNumValue = 1
60
+ this.creationTimeValue = new Date()
61
+ this.messages.clear()
62
+ }
63
+
64
+ // Message Operations
65
+
66
+ async put (record: IFixMsgStoreRecord): Promise<void> {
67
+ this.messages.set(record.seqNum, record.clone())
68
+ }
69
+
70
+ async get (seqNum: number): Promise<IFixMsgStoreRecord | null> {
71
+ const record = this.messages.get(seqNum)
72
+ return record ? record.clone() : null
73
+ }
74
+
75
+ async getRange (fromSeqNum: number, toSeqNum: number): Promise<IFixMsgStoreRecord[]> {
76
+ const results: IFixMsgStoreRecord[] = []
77
+ const keys = Array.from(this.messages.keys())
78
+ .filter(k => k >= fromSeqNum && k <= toSeqNum)
79
+ .sort((a, b) => a - b)
80
+ for (const seq of keys) {
81
+ const record = this.messages.get(seq)
82
+ if (record) {
83
+ results.push(record.clone())
84
+ }
85
+ }
86
+ return results
87
+ }
88
+
89
+ // Lifecycle
90
+
91
+ async initialize (): Promise<void> {
92
+ // No-op for memory store
93
+ }
94
+
95
+ async flush (): Promise<void> {
96
+ // No-op for memory store
97
+ }
98
+
99
+ async dispose (): Promise<void> {
100
+ // No-op for memory store
101
+ }
102
+ }
@@ -0,0 +1,97 @@
1
+ import { ISessionStreamProvider } from './session-stream-provider'
2
+
3
+ /**
4
+ * In-memory implementation of ISessionStreamProvider for testing.
5
+ * Stores all data in buffers and strings for inspection.
6
+ */
7
+ export class MemorySessionStreamProvider implements ISessionStreamProvider {
8
+ private bodyBuffer: Buffer = Buffer.alloc(0)
9
+ private headerLines: string[] = []
10
+ private seqNumsContent: string | null = null
11
+ private sessionTimeContent: string | null = null
12
+
13
+ // Inspection methods for tests
14
+
15
+ getBodyBytes (): Buffer {
16
+ return Buffer.from(this.bodyBuffer)
17
+ }
18
+
19
+ getBodyString (): string {
20
+ return this.bodyBuffer.toString('utf8')
21
+ }
22
+
23
+ getHeaderString (): string {
24
+ return this.headerLines.join('\n')
25
+ }
26
+
27
+ getHeaderLinesSnapshot (): string[] {
28
+ return [...this.headerLines]
29
+ }
30
+
31
+ getSeqNumsContent (): string | null {
32
+ return this.seqNumsContent
33
+ }
34
+
35
+ getSessionTimeContent (): string | null {
36
+ return this.sessionTimeContent
37
+ }
38
+
39
+ // ISessionStreamProvider implementation
40
+
41
+ openBody (): void {
42
+ // No-op for memory provider, body is always available
43
+ }
44
+
45
+ async appendBody (data: Buffer): Promise<number> {
46
+ const offset = this.bodyBuffer.length
47
+ this.bodyBuffer = Buffer.concat([this.bodyBuffer, data])
48
+ return offset
49
+ }
50
+
51
+ async readBody (offset: number, length: number): Promise<Buffer> {
52
+ return this.bodyBuffer.subarray(offset, offset + length)
53
+ }
54
+
55
+ getBodySize (): number {
56
+ return this.bodyBuffer.length
57
+ }
58
+
59
+ async appendHeaderLine (line: string): Promise<void> {
60
+ this.headerLines.push(line)
61
+ }
62
+
63
+ async readHeaderLines (): Promise<string[]> {
64
+ return [...this.headerLines]
65
+ }
66
+
67
+ async readSeqNums (): Promise<string | null> {
68
+ return this.seqNumsContent
69
+ }
70
+
71
+ async writeSeqNums (content: string): Promise<void> {
72
+ this.seqNumsContent = content
73
+ }
74
+
75
+ async readSessionTime (): Promise<string | null> {
76
+ return this.sessionTimeContent
77
+ }
78
+
79
+ async writeSessionTime (content: string): Promise<void> {
80
+ this.sessionTimeContent = content
81
+ }
82
+
83
+ async reset (): Promise<void> {
84
+ this.bodyBuffer = Buffer.alloc(0)
85
+ this.headerLines = []
86
+ this.seqNumsContent = null
87
+ this.sessionTimeContent = null
88
+ }
89
+
90
+ async flush (): Promise<void> {
91
+ // No-op for memory provider
92
+ }
93
+
94
+ async dispose (): Promise<void> {
95
+ // No-op for memory provider
96
+ }
97
+ }
@@ -0,0 +1,32 @@
1
+ import * as path from 'path'
2
+
3
+ /**
4
+ * Identifies a FIX session for file naming and lookup.
5
+ * Format: {BeginString}-{SenderCompID}-{TargetCompID}
6
+ */
7
+ export class SessionId {
8
+ constructor (
9
+ public readonly beginString: string,
10
+ public readonly senderCompID: string,
11
+ public readonly targetCompID: string
12
+ ) {}
13
+
14
+ /**
15
+ * Creates a file prefix for QuickFix-compatible file naming.
16
+ * Example: "FIX.4.4-SENDER-TARGET"
17
+ */
18
+ toFilePrefix (): string {
19
+ return `${this.beginString}-${this.senderCompID}-${this.targetCompID}`
20
+ }
21
+
22
+ /**
23
+ * Gets the full path for a specific file extension.
24
+ */
25
+ getFilePath (directory: string, extension: string): string {
26
+ return path.join(directory, `${this.toFilePrefix()}.${extension}`)
27
+ }
28
+
29
+ toString (): string {
30
+ return this.toFilePrefix()
31
+ }
32
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Provides stream access for session store operations.
3
+ * Allows abstraction of file I/O for testing with in-memory buffers.
4
+ */
5
+ export interface ISessionStreamProvider {
6
+ /**
7
+ * Opens or creates a read-write buffer for the message body file.
8
+ * Must support random-access reads (by offset+length).
9
+ */
10
+ openBody (): void
11
+
12
+ /**
13
+ * Appends data to the body. Returns the offset at which data was written.
14
+ */
15
+ appendBody (data: Buffer): Promise<number>
16
+
17
+ /**
18
+ * Reads data from the body at the given offset and length.
19
+ */
20
+ readBody (offset: number, length: number): Promise<Buffer>
21
+
22
+ /**
23
+ * Gets the current body size (for calculating offsets).
24
+ */
25
+ getBodySize (): number
26
+
27
+ /**
28
+ * Appends a line to the header index file.
29
+ */
30
+ appendHeaderLine (line: string): Promise<void>
31
+
32
+ /**
33
+ * Reads all lines from the header index file.
34
+ * Returns empty array if no data exists.
35
+ */
36
+ readHeaderLines (): Promise<string[]>
37
+
38
+ /**
39
+ * Reads the sequence numbers string.
40
+ * Returns null if no data exists.
41
+ */
42
+ readSeqNums (): Promise<string | null>
43
+
44
+ /**
45
+ * Writes the sequence numbers string.
46
+ */
47
+ writeSeqNums (content: string): Promise<void>
48
+
49
+ /**
50
+ * Reads the session time string.
51
+ * Returns null if no data exists.
52
+ */
53
+ readSessionTime (): Promise<string | null>
54
+
55
+ /**
56
+ * Writes the session time string.
57
+ */
58
+ writeSessionTime (content: string): Promise<void>
59
+
60
+ /**
61
+ * Resets all streams/files for a new session.
62
+ */
63
+ reset (): Promise<void>
64
+
65
+ /**
66
+ * Flushes any pending writes.
67
+ */
68
+ flush (): Promise<void>
69
+
70
+ /**
71
+ * Disposes of all resources.
72
+ */
73
+ dispose (): Promise<void>
74
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Configuration for session message store.
3
+ * Add to session description JSON to enable persistent storage.
4
+ *
5
+ * Examples:
6
+ * "store": { "type": "memory" } — explicit in-memory (default)
7
+ * "store": { "type": "file" } — file store in ./store directory
8
+ * "store": { "type": "file", "directory": "/var/fix/sessions" }
9
+ *
10
+ * Omitting the store block entirely uses in-memory storage.
11
+ */
12
+ export interface StoreConfig {
13
+ readonly type: 'memory' | 'file'
14
+ readonly directory?: string
15
+ }
@@ -2,16 +2,29 @@ import { MsgView } from '../../buffer'
2
2
  import { MsgTag, MsgType, SessionRejectReason } from '../../types'
3
3
  import { IJsFixConfig } from '../../config'
4
4
  import { FixSession } from '../session/fix-session'
5
- import { FixMsgAsciiStoreResend, FixMsgMemoryStore, IFixMsgStore, IFixMsgStoreRecord } from '../../store'
5
+ import {
6
+ FixMsgAsciiStoreResend, FixMsgMemoryStore, FixMsgStoreRecord,
7
+ IFixMsgStore, IFixMsgStoreRecord,
8
+ IFixSessionStore, IFixSessionStoreFactory,
9
+ MemorySessionStoreFactory, FileSessionStoreFactory, SessionId
10
+ } from '../../store'
6
11
  import { SessionState } from '../tcp'
7
12
  import { TickAction } from '../tick-action'
8
13
  import { IMsgApplication } from '../msg-application'
9
14
  import { SegmentType } from '../../buffer/segment/segment-type'
15
+ import { SessionSequenceCoordinator } from '../session/session-sequence-coordinator'
16
+ import { DefaultFixClock } from '../session/fix-clock'
17
+ import { ResendActionType } from '../session/resend-request-manager'
18
+ import { AsciiMsgTransmitter } from './ascii-msg-transmitter'
19
+ import { ILooseObject } from '../../collections/collection'
10
20
 
11
21
  export abstract class AsciiSession extends FixSession {
12
22
  public heartbeat: boolean = true
13
23
  protected store: IFixMsgStore | null = null
14
24
  protected resender: FixMsgAsciiStoreResend
25
+ protected readonly coordinator: SessionSequenceCoordinator
26
+ protected readonly sessionStore: IFixSessionStore
27
+ protected readonly sessionId: SessionId
15
28
 
16
29
  protected constructor (public readonly config: IJsFixConfig) {
17
30
  super(config)
@@ -19,6 +32,21 @@ export abstract class AsciiSession extends FixSession {
19
32
  this.requestLogonType = MsgType.Logon
20
33
  this.store = new FixMsgMemoryStore(this.config.description.SenderCompId, this.config)
21
34
  this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
35
+
36
+ // Create session store from factory.
37
+ // Priority: programmatic config > JSON store config > default in-memory
38
+ const storeFactory = config.sessionStoreFactory ?? AsciiSession.createStoreFactory(config.description.store)
39
+ this.sessionId = new SessionId(
40
+ config.description.BeginString,
41
+ config.description.SenderCompId,
42
+ config.description.TargetCompID
43
+ )
44
+ this.sessionStore = storeFactory.create(this.sessionId)
45
+
46
+ const clock = new DefaultFixClock()
47
+ this.coordinator = new SessionSequenceCoordinator(this.sessionStore, clock)
48
+ const lastReceivedSeqNum = config.description.LastReceivedSeqNum ?? 0
49
+ this.coordinator.initializeFromConfig(undefined, lastReceivedSeqNum + 1)
22
50
  }
23
51
 
24
52
  private checkSeqNo (msgType: string, view: MsgView): boolean {
@@ -27,6 +55,19 @@ export abstract class AsciiSession extends FixSession {
27
55
  return true
28
56
  }
29
57
 
58
+ case MsgType.Logon: {
59
+ // If peer sends ResetSeqNumFlag=Y, accept any sequence number.
60
+ // PeerLogon handles the full sequence reset.
61
+ if (view.getTyped(MsgTag.ResetSeqNumFlag) === true) {
62
+ this.sessionLogger.info('logon with ResetSeqNumFlag=Y, accepting regardless of sequence')
63
+ const seqNo = view.getTyped(MsgTag.MsgSeqNum) as number
64
+ this.sessionState.lastPeerMsgSeqNum = seqNo
65
+ this.coordinator.onMessageReceived(seqNo, false)
66
+ return true
67
+ }
68
+ }
69
+ // falls through
70
+
30
71
  default: {
31
72
  const state = this.sessionState
32
73
  const lastSeq: number = state.lastPeerMsgSeqNum
@@ -34,11 +75,28 @@ export abstract class AsciiSession extends FixSession {
34
75
  let ret: boolean = false
35
76
  const seqDelta: number = seqNo - lastSeq
36
77
  if (seqDelta <= 0) {
78
+ // Check if this is a PossDupFlag=Y message (resend replay) before rejecting.
79
+ // PossDupFlag messages have old sequence numbers and bypass normal checks.
80
+ const possDupFlag = view.getTyped(MsgTag.PossDupFlag) as boolean | undefined
81
+ if (possDupFlag === true) {
82
+ this.sessionLogger.debug(`message '${msgType}' has PossDupFlag=Y, bypassing sequence check`)
83
+ this.coordinator.onMessageReceived(seqNo, true)
84
+ return true
85
+ }
86
+ // Check if this is a delayed message that fills a pending gap range.
87
+ const pendingRequests = this.coordinator.pendingResendRequests
88
+ const inPendingGapRange = pendingRequests.some(p => seqNo >= p.begin && seqNo <= p.end)
89
+ if (inPendingGapRange) {
90
+ this.sessionLogger.info(`accepting delayed message seq ${seqNo} (in pending gap range)`)
91
+ this.coordinator.onMessageReceived(seqNo, false)
92
+ return true
93
+ }
37
94
  // serious problem ... drop immediately
38
95
  this.sessionLogger.warning(`terminate as seqDelta (${seqDelta}) < 0 lastSeq = ${lastSeq} seqNo = ${seqNo}`)
39
96
  this.stop()
40
97
  } else if (seqDelta > 1) {
41
98
  // resend request required as have missed messages.
99
+ const expectedSeq = lastSeq + 1
42
100
 
43
101
  // We process a Logon beforehand to confirm the connection even we out of sync
44
102
  if (msgType === MsgType.Logon) {
@@ -49,10 +107,43 @@ export abstract class AsciiSession extends FixSession {
49
107
  if (msgType === MsgType.ResendRequest) {
50
108
  this.onResendRequest(view)
51
109
  }
52
- this.sendResendRequest(lastSeq, seqNo)
110
+
111
+ // Use coordinator to determine what action to take for the gap
112
+ const action = this.coordinator.onGapDetected(expectedSeq, seqNo)
113
+ this.sessionLogger.info(`gap action: ${action}`)
114
+
115
+ switch (action.type) {
116
+ case ResendActionType.SendResendRequest: {
117
+ if (action.begin != null && action.end != null) {
118
+ this.sendResendRequest(lastSeq, seqNo)
119
+ this.coordinator.recordResendRequestSent(action.begin, action.end)
120
+ }
121
+ break
122
+ }
123
+ case ResendActionType.Wait: {
124
+ this.sessionLogger.info(`waiting for existing resend request: ${action.reason}`)
125
+ break
126
+ }
127
+ case ResendActionType.SendGapFill: {
128
+ this.sessionLogger.warning(`gap recovery abandoned (storm protection): ${action.reason}`)
129
+ break
130
+ }
131
+ }
132
+
133
+ // Accept the current message — don't block waiting for gap fill.
134
+ // The gap will be filled by the resend response, but this message is valid.
135
+ ret = true
136
+ state.lastPeerMsgSeqNum = seqNo
137
+ this.coordinator.onMessageReceived(seqNo, false)
53
138
  } else {
54
139
  ret = true
55
140
  state.lastPeerMsgSeqNum = seqNo
141
+ this.coordinator.onMessageReceived(seqNo, false)
142
+ }
143
+
144
+ // Reset timeout recovery on successful message receipt
145
+ if (ret) {
146
+ this.coordinator.resetTimeoutRecovery()
56
147
  }
57
148
  return ret
58
149
  }
@@ -180,6 +271,51 @@ export abstract class AsciiSession extends FixSession {
180
271
  })
181
272
  }
182
273
 
274
+ protected override onPrepareForReconnect (): void {
275
+ this.coordinator.prepareForReconnect()
276
+ this.sessionLogger.info('coordinator reset transient state for reconnect')
277
+ }
278
+
279
+ protected override txOnEncoded (msgType: string, data: string, hdr: ILooseObject): void {
280
+ super.txOnEncoded(msgType, data, hdr)
281
+ // Store the encoded message in the session store for recovery/resend
282
+ const seqNum = hdr?.MsgSeqNum as number | undefined
283
+ if (seqNum != null) {
284
+ const record = new FixMsgStoreRecord(msgType, new Date(), seqNum, undefined, data)
285
+ this.sessionStore.put(record).catch((e: Error) => {
286
+ // Never block sends on store errors
287
+ this.sessionLogger.warning(`failed to store message seq=${seqNum}: ${e.message}`)
288
+ })
289
+ }
290
+ }
291
+
292
+ private static readonly MaxLogonRetries = 100
293
+ private static readonly MaxTimeoutRecoveryAttempts = 0
294
+
295
+ private static createStoreFactory (storeConfig?: { type: string, directory?: string }): IFixSessionStoreFactory {
296
+ if (!storeConfig) return new MemorySessionStoreFactory()
297
+ switch (storeConfig.type?.toLowerCase()) {
298
+ case 'file':
299
+ return new FileSessionStoreFactory(storeConfig.directory ?? 'store')
300
+ default:
301
+ return new MemorySessionStoreFactory()
302
+ }
303
+ }
304
+
305
+ private handleLogonRejected (text: string | null): void {
306
+ if (!this.coordinator.onLogonRejectedForSequence(AsciiSession.MaxLogonRetries)) {
307
+ this.sessionLogger.warning(`max logon retries (${AsciiSession.MaxLogonRetries}) exceeded, giving up. Text='${text}'`)
308
+ this.setState(SessionState.PeerLogonRejected)
309
+ this.stop()
310
+ return
311
+ }
312
+
313
+ // The encoder's msgSeqNum is already incremented after each message is sent,
314
+ // so we just need to retry the logon. The next logon will use the next sequence number.
315
+ this.sessionLogger.info(`LOGON_SEQ_RETRY: attempt=${this.coordinator.logonRetryCount}/${AsciiSession.MaxLogonRetries}, reason='${text}'`)
316
+ this.sendLogon()
317
+ }
318
+
183
319
  okForLogon (): boolean {
184
320
  const state = this.sessionState.state
185
321
  if (this.acceptor) {
@@ -230,14 +366,29 @@ export abstract class AsciiSession extends FixSession {
230
366
 
231
367
  case MsgType.SequenceReset: {
232
368
  const newSeqNo: number = view.getTyped(MsgTag.NewSeqNo) as number
369
+ const gapFillSeq: number = view.getTyped(MsgTag.MsgSeqNum) as number
233
370
  logger.info(`peer sends '${msgType}' sequence reset. newSeqNo = ${newSeqNo}`)
234
371
  // expect newSeqNo to be the next message's sequence number.
235
372
  this.sessionState.lastPeerMsgSeqNum = newSeqNo - 1
373
+ // Notify coordinator to update expected target and clear pending resend requests
374
+ this.coordinator.onGapFillReceived(gapFillSeq, newSeqNo)
236
375
  break
237
376
  }
238
377
 
239
378
  case MsgType.Reject: {
240
- logger.info(`peer rejects type '${msgType}' with text '${view.getTyped(MsgTag.Text)}'`)
379
+ const refMsgType = view.getString(MsgTag.RefMsgType)
380
+ const text = view.getString(MsgTag.Text)
381
+ const reason = view.getTyped(MsgTag.SessionRejectReason) as number | undefined
382
+ logger.info(`peer rejects RefMsgType='${refMsgType}', reason=${reason}, text='${text}'`)
383
+
384
+ // Check if this is a logon rejection due to sequence mismatch while we're waiting for logon response.
385
+ // Only retry for ValueIsIncorrect (sequence too low) — structural rejections (RequiredTagMissing etc.)
386
+ // indicate a config problem that retrying won't fix.
387
+ if (refMsgType === MsgType.Logon &&
388
+ this.sessionState.state === SessionState.InitiationLogonSent &&
389
+ reason === SessionRejectReason.ValueIsIncorrect) {
390
+ this.handleLogonRejected(text)
391
+ }
241
392
  break
242
393
  }
243
394
  }
@@ -291,7 +442,34 @@ export abstract class AsciiSession extends FixSession {
291
442
  private peerLogon (view: MsgView): void {
292
443
  const logger = this.sessionLogger
293
444
  const [heartBtInt, peerCompId, userName, password] = view.getTypedTags([MsgTag.HeartBtInt, MsgTag.SenderCompID, MsgTag.Username, MsgTag.Password])
294
- logger.info(`peerLogon Username = ${userName}, heartBtInt = ${heartBtInt}, peerCompId = ${peerCompId}, userName = ${userName}`)
445
+ const resetSeqNumFlag = view.getTyped(MsgTag.ResetSeqNumFlag) as boolean | undefined
446
+ logger.info(`peerLogon Username = ${userName}, heartBtInt = ${heartBtInt}, peerCompId = ${peerCompId}, resetSeqNumFlag = ${resetSeqNumFlag}`)
447
+
448
+ // Handle ResetSeqNumFlag from peer's logon
449
+ if (resetSeqNumFlag === true) {
450
+ const peerSeqNum = (view.getTyped(MsgTag.MsgSeqNum) as number) ?? 1
451
+ const weAlsoReset = this.config.description.ResetSeqNumFlag
452
+ logger.info(`peer sent ResetSeqNumFlag=Y with seqNum=${peerSeqNum}, weAlsoReset=${weAlsoReset}`)
453
+
454
+ const transmitter = this.transport?.transmitter as AsciiMsgTransmitter | undefined
455
+ const savedEncoderSeqNum = weAlsoReset && transmitter ? transmitter.msgSeqNum : null
456
+
457
+ // Fire-and-forget the async coordinator call (store updates resolve on next microtask)
458
+ // but compute the expected values synchronously since we know the reset outcome
459
+ this.coordinator.handlePeerReset(peerSeqNum, weAlsoReset)
460
+ if (transmitter) {
461
+ transmitter.msgSeqNum = savedEncoderSeqNum ?? 1
462
+ }
463
+ this.sessionState.lastPeerMsgSeqNum = peerSeqNum
464
+
465
+ // Recreate resender with empty store
466
+ if (this.store) {
467
+ this.store.clear()
468
+ this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
469
+ }
470
+ logger.info(`reset complete: encoderSeqNum=${transmitter?.msgSeqNum}, lastPeerMsgSeqNum=${peerSeqNum}`)
471
+ }
472
+
295
473
  const state = this.sessionState
296
474
  state.peerHeartBeatSecs = view.getTyped(MsgTag.HeartBtInt) as number
297
475
  state.peerCompId = view.getTyped(MsgTag.SenderCompID) as string
@@ -301,11 +479,34 @@ export abstract class AsciiSession extends FixSession {
301
479
  if (this.acceptor) {
302
480
  this.setState(SessionState.InitiationLogonResponse)
303
481
  logger.info('acceptor responds to logon request')
482
+
483
+ // If WE (acceptor) are sending ResetSeqNumFlag=Y but peer didn't request it,
484
+ // reset our sequences before sending our logon response.
485
+ // This handles the broker-reset pattern where client sends N, we respond with Y.
486
+ const weReset = this.config.description.ResetSeqNumFlag
487
+ if (weReset && resetSeqNumFlag !== true) {
488
+ logger.info('acceptor sending ResetSeqNumFlag=Y (peer sent N), resetting sequences')
489
+ // Fire-and-forget async coordinator call, set values synchronously
490
+ this.coordinator.resetAsAcceptor()
491
+ const transmitter = this.transport?.transmitter as AsciiMsgTransmitter | undefined
492
+ if (transmitter) {
493
+ transmitter.msgSeqNum = 1
494
+ }
495
+ this.sessionState.lastPeerMsgSeqNum = 0
496
+ if (this.store) {
497
+ this.store.clear()
498
+ this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
499
+ }
500
+ }
501
+
304
502
  this.sendLogon() // if res send response else reject, terminate
305
503
  } else { // as an initiator the acceptor has responded
306
504
  logger.info('initiator receives logon response')
307
505
  this.setState(SessionState.InitiationLogonReceived)
308
506
  }
507
+ // Reset logon retry counter on successful logon
508
+ this.coordinator.resetLogonRetryCount()
509
+
309
510
  if (this.heartbeat) {
310
511
  this.startTimer()
311
512
  }
@@ -336,6 +537,8 @@ export abstract class AsciiSession extends FixSession {
336
537
  const action: TickAction = sessionState.calcAction(new Date())
337
538
  const application: IMsgApplication | null = this.transport.config.description.application ?? null
338
539
  const logger = this.sessionLogger
540
+ // Clean up timed-out resend requests
541
+ this.coordinator.tick()
339
542
 
340
543
  switch (action) {
341
544
  case TickAction.Nothing: {
@@ -356,8 +559,17 @@ export abstract class AsciiSession extends FixSession {
356
559
  }
357
560
 
358
561
  case TickAction.TerminateOnError: {
359
- logger.info(sessionState.toString())
360
- this.terminate(new Error(`${application?.name}: peer not responding`))
562
+ if (this.coordinator.incrementTimeoutRecovery(AsciiSession.MaxTimeoutRecoveryAttempts)) {
563
+ // Try to recover — reset timeout state to give session a fresh window.
564
+ // This helps survive sleep/wake scenarios where TCP connection may still be alive.
565
+ logger.info(`timeout recovery attempt ${this.coordinator.timeoutRecoveryAttempts}, resetting timeout state`)
566
+ sessionState.lastTestRequestAt = null
567
+ sessionState.lastReceivedAt = new Date()
568
+ this.setState(SessionState.ActiveNormalSession)
569
+ } else {
570
+ logger.info(sessionState.toString())
571
+ this.terminate(new Error(`${application?.name}: peer not responding`))
572
+ }
361
573
  break
362
574
  }
363
575
 
@@ -22,7 +22,7 @@ export class FixmlMsgTransmitter extends MsgTransmitter {
22
22
  }
23
23
  const fe = this.encoder as FixmlEncoder
24
24
  const factory = this.config.factory
25
- obj.StandardHeader = factory?.header()
25
+ obj.StandardHeader = factory?.header(msgType, 0, new Date())
26
26
  fe.encode(obj, msgType)
27
27
  return obj.StandardHeader
28
28
  }
@@ -4,7 +4,7 @@ import { IJsFixConfig, IJsFixLogger } from '../../config'
4
4
  import { IFixmlRequest } from '../fixml'
5
5
  import { FixDuplex, StringDuplex, StringDuplexTraits } from '../duplex'
6
6
 
7
- import * as express from 'express'
7
+ import express = require('express')
8
8
  import * as bodyParser from 'body-parser'
9
9
  import * as http from 'http'
10
10
  import { v4 as uuidv4 } from 'uuid'
@@ -31,7 +31,7 @@ export abstract class ASessionMsgFactory implements ISessionMsgFactory {
31
31
  }
32
32
 
33
33
  // see implementations Ascii and Fixml
34
- public abstract logon (userRequestId: string, isResponse: boolean): ILooseObject
34
+ public abstract logon (userRequestId?: string, isResponse?: boolean): ILooseObject
35
35
  public abstract logout (msgType: string, text: string): ILooseObject
36
36
  public abstract header (msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>): ILooseObject
37
37
 
@@ -0,0 +1,9 @@
1
+ export interface IFixClock {
2
+ now (): Date
3
+ }
4
+
5
+ export class DefaultFixClock implements IFixClock {
6
+ now (): Date {
7
+ return new Date()
8
+ }
9
+ }