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
@@ -390,9 +390,14 @@ export abstract class FixSession extends events.EventEmitter {
390
390
  const resetFlag = this.config.description.ResetSeqNumFlag
391
391
  const seqNum = resetFlag ? 0 : resetSeqNum ?? this.sessionState.lastPeerMsgSeqNum
392
392
  this.sessionState.reset(seqNum) // from header def ... eventually
393
+ this.onPrepareForReconnect()
393
394
  this.setState(SessionState.NetworkConnectionEstablished)
394
395
  }
395
396
 
397
+ protected onPrepareForReconnect (): void {
398
+ // Override in subclass to reset coordinator/transient state
399
+ }
400
+
396
401
  protected stop (error: Error | null = null): void {
397
402
  if (this.sessionState.state === SessionState.Stopped) {
398
403
  return
@@ -6,3 +6,7 @@ export * from './session-50-description'
6
6
  export * from './session-description'
7
7
  export * from './session-msg-factory'
8
8
  export * from './session-state'
9
+ export * from './session-sequence-coordinator'
10
+ export * from './session-sequence-store'
11
+ export * from './resend-request-manager'
12
+ export * from './fix-clock'
@@ -0,0 +1,268 @@
1
+ export enum ResendActionType {
2
+ Nothing = 'Nothing',
3
+ SendResendRequest = 'SendResendRequest',
4
+ SendGapFill = 'SendGapFill',
5
+ Wait = 'Wait'
6
+ }
7
+
8
+ export class ResendAction {
9
+ private constructor (
10
+ public readonly type: ResendActionType,
11
+ public readonly begin: number | null,
12
+ public readonly end: number | null,
13
+ public readonly reason: string | null
14
+ ) {}
15
+
16
+ static sendResendRequest (begin: number, end: number): ResendAction {
17
+ return new ResendAction(ResendActionType.SendResendRequest, begin, end, null)
18
+ }
19
+
20
+ static gapFill (begin: number, end: number, reason: string): ResendAction {
21
+ return new ResendAction(ResendActionType.SendGapFill, begin, end, reason)
22
+ }
23
+
24
+ static wait (reason: string): ResendAction {
25
+ return new ResendAction(ResendActionType.Wait, null, null, reason)
26
+ }
27
+
28
+ static nothing (): ResendAction {
29
+ return new ResendAction(ResendActionType.Nothing, null, null, null)
30
+ }
31
+
32
+ toString (): string {
33
+ switch (this.type) {
34
+ case ResendActionType.SendResendRequest:
35
+ return `SendResendRequest(${this.begin}-${this.end})`
36
+ case ResendActionType.SendGapFill:
37
+ return `GapFill(${this.begin}-${this.end}): ${this.reason}`
38
+ case ResendActionType.Wait:
39
+ return `Wait: ${this.reason}`
40
+ case ResendActionType.Nothing:
41
+ return 'Nothing'
42
+ default:
43
+ return `Unknown(${this.type})`
44
+ }
45
+ }
46
+ }
47
+
48
+ export class PendingResendRange {
49
+ private readonly receivedSeqs: Set<number> = new Set()
50
+
51
+ constructor (
52
+ public readonly begin: number,
53
+ public readonly end: number,
54
+ public readonly sentAt: Date
55
+ ) {}
56
+
57
+ markReceived (seqNum: number): void {
58
+ if (seqNum >= this.begin && seqNum <= this.end) {
59
+ this.receivedSeqs.add(seqNum)
60
+ }
61
+ }
62
+
63
+ markRangeReceived (fromSeq: number, toSeq: number): void {
64
+ const start = Math.max(fromSeq, this.begin)
65
+ const stop = Math.min(toSeq, this.end)
66
+ for (let seq = start; seq <= stop; seq++) {
67
+ this.receivedSeqs.add(seq)
68
+ }
69
+ }
70
+
71
+ get isFullySatisfied (): boolean {
72
+ return this.receivedSeqs.size >= (this.end - this.begin + 1)
73
+ }
74
+
75
+ fullyCovers (begin: number, end: number): boolean {
76
+ return begin >= this.begin && end <= this.end
77
+ }
78
+
79
+ overlaps (begin: number, end: number): boolean {
80
+ return begin <= this.end && end >= this.begin
81
+ }
82
+
83
+ get pendingCount (): number {
84
+ return (this.end - this.begin + 1) - this.receivedSeqs.size
85
+ }
86
+
87
+ toString (): string {
88
+ return `Pending(${this.begin}-${this.end}, received=${this.receivedSeqs.size}/${this.end - this.begin + 1})`
89
+ }
90
+ }
91
+
92
+ export interface ResendRequestRecord {
93
+ begin: number
94
+ end: number
95
+ sentAt: Date
96
+ reason: string | null
97
+ }
98
+
99
+ export interface ResendManagerConfig {
100
+ maxPendingRequests?: number
101
+ maxRequestsPerWindow?: number
102
+ rateLimitWindowSeconds?: number
103
+ requestTimeoutSeconds?: number
104
+ }
105
+
106
+ /**
107
+ * Manages ResendRequest strategy with intelligent overlap handling and storm protection.
108
+ *
109
+ * Philosophy:
110
+ * 1. Stay alive — don't let resend handling crash or overwhelm the session
111
+ * 2. Keep receiving new messages — don't block normal message flow
112
+ * 3. Don't make things worse — avoid storms, handle overlaps intelligently
113
+ */
114
+ export class ResendRequestManager {
115
+ private readonly pendingRequests: PendingResendRange[] = []
116
+ private readonly requestHistory: ResendRequestRecord[] = []
117
+ private readonly maxPendingRequests: number
118
+ private readonly maxRequestsPerWindow: number
119
+ private readonly rateLimitWindowMs: number
120
+ private readonly requestTimeoutMs: number
121
+ private readonly recentRequestTimes: Date[] = []
122
+
123
+ constructor (config?: ResendManagerConfig) {
124
+ this.maxPendingRequests = config?.maxPendingRequests ?? 1
125
+ this.maxRequestsPerWindow = config?.maxRequestsPerWindow ?? 5
126
+ this.rateLimitWindowMs = (config?.rateLimitWindowSeconds ?? 10) * 1000
127
+ this.requestTimeoutMs = (config?.requestTimeoutSeconds ?? 30) * 1000
128
+ }
129
+
130
+ computeAction (expectedSeq: number, receivedSeq: number, now: Date): ResendAction {
131
+ if (receivedSeq <= expectedSeq) {
132
+ return ResendAction.nothing()
133
+ }
134
+
135
+ const gapBegin = expectedSeq
136
+ const gapEnd = receivedSeq - 1
137
+
138
+ this.cleanupTimedOutRequests(now)
139
+
140
+ if (this.isStorming(now)) {
141
+ return ResendAction.gapFill(gapBegin, gapEnd, 'storm protection - too many requests')
142
+ }
143
+
144
+ for (const pending of this.pendingRequests) {
145
+ if (pending.fullyCovers(gapBegin, gapEnd)) {
146
+ return ResendAction.wait(`fully covered by pending request ${pending}`)
147
+ }
148
+ }
149
+
150
+ const uncovered = this.computeUncoveredRange(gapBegin, gapEnd)
151
+ if (!uncovered) {
152
+ return ResendAction.wait('gap partially covered, waiting for pending requests')
153
+ }
154
+
155
+ if (this.pendingRequests.length >= this.maxPendingRequests) {
156
+ return ResendAction.wait(`max pending requests (${this.maxPendingRequests}) reached`)
157
+ }
158
+
159
+ return ResendAction.sendResendRequest(uncovered.begin, uncovered.end)
160
+ }
161
+
162
+ recordRequestSent (begin: number, end: number, now: Date, reason: string | null = null): void {
163
+ this.pendingRequests.push(new PendingResendRange(begin, end, now))
164
+ this.requestHistory.push({ begin, end, sentAt: now, reason })
165
+ this.recentRequestTimes.push(now)
166
+ }
167
+
168
+ onMessageReceived (seqNum: number, possDupFlag: boolean, now: Date): void {
169
+ for (const pending of this.pendingRequests) {
170
+ pending.markReceived(seqNum)
171
+ }
172
+ this.removeFullySatisfied()
173
+ }
174
+
175
+ onGapFillReceived (gapFillSeq: number, newSeqNo: number, now: Date): void {
176
+ for (const pending of this.pendingRequests) {
177
+ pending.markRangeReceived(gapFillSeq, newSeqNo - 1)
178
+ }
179
+ this.removeFullySatisfied()
180
+ }
181
+
182
+ tick (now: Date): void {
183
+ this.cleanupTimedOutRequests(now)
184
+ this.cleanupRateLimitWindow(now)
185
+ }
186
+
187
+ reset (): void {
188
+ this.pendingRequests.length = 0
189
+ this.recentRequestTimes.length = 0
190
+ // keep requestHistory for debugging
191
+ }
192
+
193
+ get pending (): ReadonlyArray<PendingResendRange> {
194
+ return this.pendingRequests.slice()
195
+ }
196
+
197
+ get history (): ReadonlyArray<ResendRequestRecord> {
198
+ return this.requestHistory.slice()
199
+ }
200
+
201
+ get pendingCount (): number {
202
+ return this.pendingRequests.length
203
+ }
204
+
205
+ get hasPendingRequests (): boolean {
206
+ return this.pendingRequests.length > 0
207
+ }
208
+
209
+ private isStorming (now: Date): boolean {
210
+ this.cleanupRateLimitWindow(now)
211
+ return this.recentRequestTimes.length >= this.maxRequestsPerWindow
212
+ }
213
+
214
+ private cleanupRateLimitWindow (now: Date): void {
215
+ const cutoff = now.getTime() - this.rateLimitWindowMs
216
+ while (this.recentRequestTimes.length > 0 && this.recentRequestTimes[0].getTime() < cutoff) {
217
+ this.recentRequestTimes.shift()
218
+ }
219
+ }
220
+
221
+ private cleanupTimedOutRequests (now: Date): void {
222
+ const cutoff = now.getTime() - this.requestTimeoutMs
223
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
224
+ if (this.pendingRequests[i].sentAt.getTime() < cutoff) {
225
+ this.pendingRequests.splice(i, 1)
226
+ }
227
+ }
228
+ }
229
+
230
+ private removeFullySatisfied (): void {
231
+ for (let i = this.pendingRequests.length - 1; i >= 0; i--) {
232
+ if (this.pendingRequests[i].isFullySatisfied) {
233
+ this.pendingRequests.splice(i, 1)
234
+ }
235
+ }
236
+ }
237
+
238
+ private computeUncoveredRange (gapBegin: number, gapEnd: number): { begin: number, end: number } | null {
239
+ if (this.pendingRequests.length === 0) {
240
+ return { begin: gapBegin, end: gapEnd }
241
+ }
242
+
243
+ const covered: Set<number> = new Set()
244
+ for (const pending of this.pendingRequests) {
245
+ for (let seq = pending.begin; seq <= pending.end; seq++) {
246
+ covered.add(seq)
247
+ }
248
+ }
249
+
250
+ let uncoveredBegin: number | null = null
251
+ let uncoveredEnd: number | null = null
252
+
253
+ for (let seq = gapBegin; seq <= gapEnd; seq++) {
254
+ if (!covered.has(seq)) {
255
+ if (uncoveredBegin === null) uncoveredBegin = seq
256
+ uncoveredEnd = seq
257
+ } else if (uncoveredBegin !== null) {
258
+ break
259
+ }
260
+ }
261
+
262
+ if (uncoveredBegin !== null && uncoveredEnd !== null) {
263
+ return { begin: uncoveredBegin, end: uncoveredEnd }
264
+ }
265
+
266
+ return null
267
+ }
268
+ }
@@ -1,4 +1,5 @@
1
1
  import { IMsgApplication } from '../msg-application'
2
+ import { StoreConfig } from '../../store/store-config'
2
3
 
3
4
  export interface IDynamicSessionParams {
4
5
  readonly Name: string
@@ -18,4 +19,5 @@ export interface ISessionDescription extends IDynamicSessionParams {
18
19
  LastSentSeqNum?: number
19
20
  readonly LastReceivedSeqNum?: number
20
21
  readonly BodyLengthChars?: number
22
+ readonly store?: StoreConfig
21
23
  }
@@ -11,6 +11,6 @@ export interface ISessionMsgFactory {
11
11
  resendRequest: (from: number, to: number) => ILooseObject
12
12
  sequenceReset: (newSeq: number, gapFill?: boolean) => ILooseObject
13
13
  heartbeat: (testReqId: string) => ILooseObject
14
- header: (msgType?: string, seqNum?: number, time?: Date, overrideData?: Partial<IStandardHeader>) => ILooseObject
14
+ header: (msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>) => ILooseObject
15
15
  trailer: (checksum: number) => ILooseObject
16
16
  }
@@ -0,0 +1,272 @@
1
+ import { ISessionSequenceStore } from './session-sequence-store'
2
+ import { IFixClock } from './fix-clock'
3
+ import { ResendAction, ResendManagerConfig, ResendRequestManager, PendingResendRange } from './resend-request-manager'
4
+
5
+ /**
6
+ * Coordinates all sequence-related state for a FIX session.
7
+ *
8
+ * This is the SINGLE SOURCE OF TRUTH for:
9
+ * - Outgoing sequence numbers (what we send)
10
+ * - Expected incoming sequence numbers (what we expect from peer)
11
+ * - Resend request tracking
12
+ * - Reset coordination
13
+ *
14
+ * Philosophy:
15
+ * 1. All sequence state lives here — no scattered state across components
16
+ * 2. All reset operations are coordinated through here
17
+ * 3. Components query this for sequence numbers, don't maintain their own
18
+ * 4. Fully testable without running actual sessions
19
+ */
20
+ export class SessionSequenceCoordinator {
21
+ private readonly store: ISessionSequenceStore
22
+ private readonly resendManager: ResendRequestManager
23
+ private readonly clock: IFixClock
24
+
25
+ // THE source of truth for sequence numbers
26
+ private nextSenderSeqNumValue: number = 1
27
+ private expectedTargetSeqNumValue: number = 1
28
+
29
+ // Track the last peer sequence we actually processed
30
+ private lastProcessedPeerSeqNumValue: number = 0
31
+
32
+ // Transient session state (reset on reconnect)
33
+ private logonRetryCountValue: number = 0
34
+ private timeoutRecoveryAttemptsValue: number = 0
35
+
36
+ // Event callback for full session reset
37
+ public onSessionReset: (() => Promise<void>) | null = null
38
+
39
+ constructor (
40
+ store: ISessionSequenceStore,
41
+ clock: IFixClock,
42
+ resendManagerConfig?: ResendManagerConfig
43
+ ) {
44
+ this.store = store
45
+ this.clock = clock
46
+ this.resendManager = new ResendRequestManager(resendManagerConfig)
47
+ }
48
+
49
+ // ── Initialization ──
50
+
51
+ initializeFromStore (): void {
52
+ this.nextSenderSeqNumValue = this.store.senderSeqNum
53
+ this.expectedTargetSeqNumValue = this.store.targetSeqNum
54
+ this.lastProcessedPeerSeqNumValue = this.expectedTargetSeqNumValue - 1
55
+ }
56
+
57
+ initializeFromConfig (senderSeqNum?: number, targetSeqNum?: number): void {
58
+ if (senderSeqNum !== undefined) {
59
+ this.nextSenderSeqNumValue = senderSeqNum
60
+ }
61
+ if (targetSeqNum !== undefined) {
62
+ this.expectedTargetSeqNumValue = targetSeqNum
63
+ this.lastProcessedPeerSeqNumValue = targetSeqNum - 1
64
+ }
65
+ }
66
+
67
+ // ── Sequence Access (Read) ──
68
+
69
+ get nextSenderSeqNum (): number {
70
+ return this.nextSenderSeqNumValue
71
+ }
72
+
73
+ get expectedTargetSeqNum (): number {
74
+ return this.expectedTargetSeqNumValue
75
+ }
76
+
77
+ get lastProcessedPeerSeqNum (): number {
78
+ return this.lastProcessedPeerSeqNumValue
79
+ }
80
+
81
+ // ── Sequence Mutations (Controlled) ──
82
+
83
+ /**
84
+ * Consumes and returns the next sender sequence number.
85
+ * Call when encoding a message (not for PossDup resends).
86
+ */
87
+ getNextSenderSeqNum (isPossDup: boolean = false): number {
88
+ const seq = this.nextSenderSeqNumValue
89
+ if (!isPossDup) {
90
+ this.nextSenderSeqNumValue++
91
+ }
92
+ return seq
93
+ }
94
+
95
+ /**
96
+ * Records that a message was successfully encoded and will be sent.
97
+ * Updates the store's sender sequence number.
98
+ */
99
+ async onMessageEncoded (seqNum: number, isPossDup: boolean): Promise<void> {
100
+ if (isPossDup) return
101
+ await this.store.setSenderSeqNum(seqNum + 1)
102
+ }
103
+
104
+ /**
105
+ * Called when a message is received from the peer.
106
+ * Updates expected sequence and resend tracking.
107
+ * Returns true if message should be processed, false if duplicate/old.
108
+ */
109
+ async onMessageReceived (seqNum: number, possDupFlag: boolean): Promise<boolean> {
110
+ const now = this.clock.now()
111
+
112
+ this.resendManager.onMessageReceived(seqNum, possDupFlag, now)
113
+
114
+ if (possDupFlag) {
115
+ return true
116
+ }
117
+
118
+ if (seqNum < this.expectedTargetSeqNumValue) {
119
+ return false
120
+ }
121
+
122
+ if (seqNum === this.expectedTargetSeqNumValue) {
123
+ this.lastProcessedPeerSeqNumValue = seqNum
124
+ this.expectedTargetSeqNumValue = seqNum + 1
125
+ } else if (seqNum > this.expectedTargetSeqNumValue) {
126
+ // Gap detected — session accepts this message and moves on
127
+ this.lastProcessedPeerSeqNumValue = seqNum
128
+ this.expectedTargetSeqNumValue = seqNum + 1
129
+ }
130
+
131
+ await this.store.setTargetSeqNum(this.expectedTargetSeqNumValue)
132
+ return true
133
+ }
134
+
135
+ /**
136
+ * Called when a SequenceReset-GapFill is received.
137
+ */
138
+ async onGapFillReceived (gapFillSeq: number, newSeqNo: number): Promise<void> {
139
+ const now = this.clock.now()
140
+ this.resendManager.onGapFillReceived(gapFillSeq, newSeqNo, now)
141
+
142
+ // GapFill says "skip from gapFillSeq to newSeqNo" — never rewind
143
+ if (newSeqNo > this.expectedTargetSeqNumValue) {
144
+ this.expectedTargetSeqNumValue = newSeqNo
145
+ this.lastProcessedPeerSeqNumValue = Math.max(this.lastProcessedPeerSeqNumValue, newSeqNo - 1)
146
+ }
147
+
148
+ await this.store.setTargetSeqNum(this.expectedTargetSeqNumValue)
149
+ }
150
+
151
+ // ── Gap Detection and Resend Requests ──
152
+
153
+ onGapDetected (expectedSeq: number, receivedSeq: number): ResendAction {
154
+ const now = this.clock.now()
155
+ return this.resendManager.computeAction(expectedSeq, receivedSeq, now)
156
+ }
157
+
158
+ recordResendRequestSent (begin: number, end: number): void {
159
+ const now = this.clock.now()
160
+ this.resendManager.recordRequestSent(begin, end, now)
161
+ }
162
+
163
+ get pendingResendRequests (): ReadonlyArray<PendingResendRange> {
164
+ return this.resendManager.pending
165
+ }
166
+
167
+ // ── Logon Retry Logic ──
168
+
169
+ onLogonRejectedForSequence (maxRetries: number = 10): boolean {
170
+ this.logonRetryCountValue++
171
+ if (this.logonRetryCountValue <= maxRetries) {
172
+ this.nextSenderSeqNumValue++
173
+ return true
174
+ }
175
+ return false
176
+ }
177
+
178
+ resetLogonRetryCount (): void {
179
+ this.logonRetryCountValue = 0
180
+ }
181
+
182
+ get logonRetryCount (): number {
183
+ return this.logonRetryCountValue
184
+ }
185
+
186
+ // ── Timeout Recovery ──
187
+
188
+ incrementTimeoutRecovery (maxAttempts: number = 3): boolean {
189
+ this.timeoutRecoveryAttemptsValue++
190
+ return this.timeoutRecoveryAttemptsValue <= maxAttempts
191
+ }
192
+
193
+ resetTimeoutRecovery (): void {
194
+ this.timeoutRecoveryAttemptsValue = 0
195
+ }
196
+
197
+ get timeoutRecoveryAttempts (): number {
198
+ return this.timeoutRecoveryAttemptsValue
199
+ }
200
+
201
+ // ── Reset Operations ──
202
+
203
+ /**
204
+ * Prepares for reconnection on the same session.
205
+ * Resets transient state but preserves sequence numbers.
206
+ */
207
+ prepareForReconnect (): void {
208
+ this.logonRetryCountValue = 0
209
+ this.timeoutRecoveryAttemptsValue = 0
210
+ this.resendManager.reset()
211
+ }
212
+
213
+ /**
214
+ * Full session reset — clears store, resets sequences to 1.
215
+ * Call when ResetSeqNumFlag=Y is being used.
216
+ */
217
+ async resetSession (reason: string): Promise<void> {
218
+ this.nextSenderSeqNumValue = 1
219
+ this.expectedTargetSeqNumValue = 1
220
+ this.lastProcessedPeerSeqNumValue = 0
221
+ this.logonRetryCountValue = 0
222
+ this.timeoutRecoveryAttemptsValue = 0
223
+ this.resendManager.reset()
224
+
225
+ await this.store.reset()
226
+
227
+ if (this.onSessionReset) {
228
+ await this.onSessionReset()
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Handles peer's ResetSeqNumFlag=Y in their Logon.
234
+ */
235
+ async handlePeerReset (peerSeqNum: number, weAlsoReset: boolean): Promise<void> {
236
+ const savedSenderSeq = weAlsoReset ? this.nextSenderSeqNumValue : null
237
+
238
+ await this.store.reset()
239
+
240
+ this.nextSenderSeqNumValue = savedSenderSeq ?? this.store.senderSeqNum
241
+ this.expectedTargetSeqNumValue = peerSeqNum + 1
242
+ this.lastProcessedPeerSeqNumValue = peerSeqNum
243
+ this.resendManager.reset()
244
+
245
+ await this.store.setTargetSeqNum(this.expectedTargetSeqNumValue)
246
+
247
+ if (this.onSessionReset) {
248
+ await this.onSessionReset()
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Handles acceptor responding with ResetSeqNumFlag=Y when peer didn't request it.
254
+ */
255
+ async resetAsAcceptor (): Promise<void> {
256
+ this.nextSenderSeqNumValue = 1
257
+ this.expectedTargetSeqNumValue = 1
258
+ this.lastProcessedPeerSeqNumValue = 0
259
+ this.resendManager.reset()
260
+
261
+ await this.store.reset()
262
+ await this.store.setTargetSeqNum(1)
263
+ }
264
+
265
+ /**
266
+ * Periodic tick — cleans up stale resend requests.
267
+ */
268
+ tick (): void {
269
+ const now = this.clock.now()
270
+ this.resendManager.tick(now)
271
+ }
272
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Minimal store interface for the SessionSequenceCoordinator.
3
+ * Both the existing FixMsgMemoryStore (adapted) and the future
4
+ * IFixSessionStore will implement this.
5
+ */
6
+ export interface ISessionSequenceStore {
7
+ senderSeqNum: number
8
+ targetSeqNum: number
9
+ setSenderSeqNum (value: number): Promise<void>
10
+ setTargetSeqNum (value: number): Promise<void>
11
+ reset (): Promise<void>
12
+ }
13
+
14
+ /**
15
+ * Simple in-memory implementation for testing and as default.
16
+ */
17
+ export class MemorySequenceStore implements ISessionSequenceStore {
18
+ public senderSeqNum: number = 1
19
+ public targetSeqNum: number = 1
20
+
21
+ async setSenderSeqNum (value: number): Promise<void> {
22
+ this.senderSeqNum = value
23
+ }
24
+
25
+ async setTargetSeqNum (value: number): Promise<void> {
26
+ this.targetSeqNum = value
27
+ }
28
+
29
+ async reset (): Promise<void> {
30
+ this.senderSeqNum = 1
31
+ this.targetSeqNum = 1
32
+ }
33
+ }
@@ -55,7 +55,7 @@ export class TcpAcceptor extends FixAcceptor {
55
55
  }
56
56
  })
57
57
  } catch (e) {
58
- this.logger.error(e)
58
+ this.logger.error(e as Error)
59
59
  throw e
60
60
  }
61
61
  }
@@ -71,7 +71,7 @@ export class TcpAcceptor extends FixAcceptor {
71
71
  this.onSocket(id, socket, config)
72
72
  })
73
73
  } catch (e) {
74
- this.logger.error(e)
74
+ this.logger.error(e as Error)
75
75
  throw e
76
76
  }
77
77
  }