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.
- package/BACKPORT_PLAN.md +138 -79
- package/dist/buffer/fixml/fixml-view.js.map +1 -1
- package/dist/buffer/msg-encoder.js +34 -1
- package/dist/buffer/msg-encoder.js.map +1 -1
- package/dist/buffer/msg-parser.js +34 -1
- package/dist/buffer/msg-parser.js.map +1 -1
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/collections/index.js +1 -0
- package/dist/config/js-fix-config.d.ts +2 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/config/winston-logger.js.map +1 -1
- package/dist/dict-parser.js +34 -1
- package/dist/dict-parser.js.map +1 -1
- package/dist/dictionary/compiler/enum-compiler.js +37 -4
- package/dist/dictionary/compiler/enum-compiler.js.map +1 -1
- package/dist/dictionary/compiler/msg-compiler.js +36 -3
- package/dist/dictionary/compiler/msg-compiler.js.map +1 -1
- package/dist/dictionary/compiler/standard-snippet.js +34 -1
- package/dist/dictionary/compiler/standard-snippet.js.map +1 -1
- package/dist/dictionary/contained/contained-field-set.js +2 -0
- package/dist/dictionary/contained/contained-field-set.js.map +1 -1
- package/dist/dictionary/definition/simple-field-definition.js +34 -1
- package/dist/dictionary/definition/simple-field-definition.js.map +1 -1
- package/dist/dictionary/fix-parser.js +34 -1
- package/dist/dictionary/fix-parser.js.map +1 -1
- package/dist/dictionary/parser/fix-repository/repository-type.js +1 -0
- package/dist/dictionary/parser/fix-repository/repository-xml-parser.js +35 -2
- package/dist/dictionary/parser/fix-repository/repository-xml-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/fields-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/fix-xsd-parser.js +34 -1
- package/dist/dictionary/parser/fixml/fix-xsd-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/include-graph.js +35 -2
- package/dist/dictionary/parser/fixml/include-graph.js.map +1 -1
- package/dist/dictionary/parser/fixml/node-definitions.js +1 -0
- package/dist/dictionary/parser/fixml/xsd-parser.js +34 -1
- package/dist/dictionary/parser/fixml/xsd-parser.js.map +1 -1
- package/dist/jsfix-cmd.js +39 -3
- package/dist/jsfix-cmd.js.map +1 -1
- package/dist/runtime/session-launcher.js +34 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/sample/http/oms/app.js +34 -1
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/recovering-skeleton/app.js +34 -1
- package/dist/sample/tcp/recovering-skeleton/app.js.map +1 -1
- package/dist/store/file-session-store.d.ts +42 -0
- package/dist/store/file-session-store.js +256 -0
- package/dist/store/file-session-store.js.map +1 -0
- package/dist/store/file-session-stream-provider.d.ts +25 -0
- package/dist/store/file-session-stream-provider.js +162 -0
- package/dist/store/file-session-stream-provider.js.map +1 -0
- package/dist/store/fix-msg-ascii-store-resend.js +1 -1
- package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +13 -0
- package/dist/store/fix-session-store-factory.js +21 -0
- package/dist/store/fix-session-store-factory.js.map +1 -0
- package/dist/store/fix-session-store.d.ts +19 -0
- package/dist/store/fix-session-store.js +3 -0
- package/dist/store/fix-session-store.js.map +1 -0
- package/dist/store/index.d.ts +9 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/memory-session-store.d.ts +27 -0
- package/dist/store/memory-session-store.js +104 -0
- package/dist/store/memory-session-store.js.map +1 -0
- package/dist/store/memory-session-stream-provider.d.ts +26 -0
- package/dist/store/memory-session-stream-provider.js +103 -0
- package/dist/store/memory-session-stream-provider.js.map +1 -0
- package/dist/store/session-id.d.ts +9 -0
- package/dist/store/session-id.js +55 -0
- package/dist/store/session-id.js.map +1 -0
- package/dist/store/session-stream-provider.d.ts +15 -0
- package/dist/store/session-stream-provider.js +3 -0
- package/dist/store/session-stream-provider.js.map +1 -0
- package/dist/store/store-config.d.ts +4 -0
- package/dist/store/store-config.js +3 -0
- package/dist/store/store-config.js.map +1 -0
- package/dist/transport/ascii/ascii-session.d.ts +12 -1
- package/dist/transport/ascii/ascii-session.js +154 -5
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/http-duplex.js +4 -1
- package/dist/transport/duplex/http-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.js +34 -1
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/fix-acceptor.js +34 -1
- package/dist/transport/fix-acceptor.js.map +1 -1
- package/dist/transport/fix-entity.js +34 -1
- package/dist/transport/fix-entity.js.map +1 -1
- package/dist/transport/fixml/fixml-msg-transmitter.js +1 -1
- package/dist/transport/fixml/fixml-msg-transmitter.js.map +1 -1
- package/dist/transport/http/http-acceptor.js +34 -1
- package/dist/transport/http/http-acceptor.js.map +1 -1
- package/dist/transport/msg-transmitter.js +34 -1
- package/dist/transport/msg-transmitter.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -1
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/fix-clock.d.ts +6 -0
- package/dist/transport/session/fix-clock.js +10 -0
- package/dist/transport/session/fix-clock.js.map +1 -0
- package/dist/transport/session/fix-session.d.ts +1 -0
- package/dist/transport/session/fix-session.js +37 -1
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +4 -0
- package/dist/transport/session/index.js +4 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +69 -0
- package/dist/transport/session/resend-request-manager.js +208 -0
- package/dist/transport/session/resend-request-manager.js.map +1 -0
- package/dist/transport/session/session-description.d.ts +2 -0
- package/dist/transport/session/session-description.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -1
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-sequence-coordinator.d.ts +38 -0
- package/dist/transport/session/session-sequence-coordinator.js +180 -0
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -0
- package/dist/transport/session/session-sequence-store.d.ts +14 -0
- package/dist/transport/session/session-sequence-store.js +36 -0
- package/dist/transport/session/session-sequence-store.js.map +1 -0
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +34 -1
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/types/FIX4.4/index.js +1 -0
- package/dist/util/buffer-helper.js +34 -1
- package/dist/util/buffer-helper.js.map +1 -1
- package/dist/util/definition-factory.js +35 -2
- package/dist/util/definition-factory.js.map +1 -1
- package/jsfix.test_client.txt +67 -66
- package/jsfix.test_server.txt +64 -63
- package/package.json +11 -10
- package/src/buffer/fixml/fixml-view.ts +1 -1
- package/src/buffer/msg-view.ts +1 -1
- package/src/config/js-fix-config.ts +2 -0
- package/src/config/winston-logger.ts +3 -3
- package/src/dictionary/contained/contained-field-set.ts +2 -1
- package/src/dictionary/parser/fixml/fields-parser.ts +2 -2
- package/src/jsfix-cmd.ts +1 -1
- package/src/store/file-session-store.ts +294 -0
- package/src/store/file-session-stream-provider.ts +123 -0
- package/src/store/fix-msg-ascii-store-resend.ts +1 -1
- package/src/store/fix-session-store-factory.ts +31 -0
- package/src/store/fix-session-store.ts +37 -0
- package/src/store/index.ts +9 -0
- package/src/store/memory-session-store.ts +102 -0
- package/src/store/memory-session-stream-provider.ts +97 -0
- package/src/store/session-id.ts +32 -0
- package/src/store/session-stream-provider.ts +74 -0
- package/src/store/store-config.ts +15 -0
- package/src/transport/ascii/ascii-session.ts +218 -6
- package/src/transport/fixml/fixml-msg-transmitter.ts +1 -1
- package/src/transport/http/http-acceptor.ts +1 -1
- package/src/transport/session/a-session-msg-factory.ts +1 -1
- package/src/transport/session/fix-clock.ts +9 -0
- package/src/transport/session/fix-session.ts +5 -0
- package/src/transport/session/index.ts +4 -0
- package/src/transport/session/resend-request-manager.ts +268 -0
- package/src/transport/session/session-description.ts +2 -0
- package/src/transport/session/session-msg-factory.ts +1 -1
- package/src/transport/session/session-sequence-coordinator.ts +272 -0
- package/src/transport/session/session-sequence-store.ts +33 -0
- 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
|
|
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
|
}
|