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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
|
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
|
|
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
|
|