jspurefix 5.8.5 → 5.9.2
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/.gitattributes +4 -0
- package/BACKPORT_PLAN.md +84 -0
- package/DEMO_PORT_PLAN.md +10 -0
- package/README.md +3 -0
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/config/js-fix-config.d.ts +4 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/runtime/session-scope.d.ts +3 -0
- package/dist/runtime/session-scope.js +47 -0
- package/dist/runtime/session-scope.js.map +1 -0
- package/dist/sample/http/oms/app.js +3 -3
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/qf-md/app.js +3 -3
- package/dist/sample/tcp/qf-md/app.js.map +1 -1
- package/dist/sample/tcp/skeleton/app.js +1 -1
- package/dist/sample/tcp/skeleton/app.js.map +1 -1
- package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
- package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +17 -3
- package/dist/transport/ascii/ascii-session.js +121 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/fix-duplex.d.ts +1 -0
- package/dist/transport/duplex/fix-duplex.js +3 -0
- package/dist/transport/duplex/fix-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
- package/dist/transport/duplex/tcp-duplex.js +5 -0
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/factory/msg-transport.d.ts +2 -0
- package/dist/transport/factory/msg-transport.js +17 -2
- package/dist/transport/factory/msg-transport.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
- package/dist/transport/session/a-session-msg-factory.js +4 -0
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/clone-session-description.d.ts +6 -0
- package/dist/transport/session/clone-session-description.js +11 -0
- package/dist/transport/session/clone-session-description.js.map +1 -0
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +5 -1
- package/dist/transport/session/fix-session.js +93 -24
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +2 -0
- package/dist/transport/session/index.js +2 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -0
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-registry.d.ts +23 -0
- package/dist/transport/session/session-registry.js +59 -0
- package/dist/transport/session/session-registry.js.map +1 -0
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
- package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
- package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
- package/dist/transport/tcp/tcp-acceptor.js +74 -20
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +15 -5
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
- package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
- package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
- package/dist/transport/tcp/tls-diagnostics.js +64 -0
- package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
- package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
- package/dist/transport/tcp/tls-options-factory.js +24 -2
- package/dist/transport/tcp/tls-options-factory.js.map +1 -1
- package/docs/acceptor.md +207 -0
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -75
- package/jsfix.test_server.txt +72 -72
- package/package.json +19 -10
- package/script/pack-local.js +68 -0
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/config/js-fix-config.ts +8 -0
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/runtime/session-launcher.ts +5 -1
- package/src/runtime/session-scope.ts +84 -0
- package/src/sample/http/oms/app.ts +3 -3
- package/src/sample/tcp/qf-md/app.ts +3 -3
- package/src/sample/tcp/skeleton/app.ts +1 -1
- package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/ascii/ascii-session.ts +186 -11
- package/src/transport/duplex/fix-duplex.ts +9 -0
- package/src/transport/duplex/tcp-duplex.ts +10 -0
- package/src/transport/factory/msg-transport.ts +20 -0
- package/src/transport/session/a-session-msg-factory.ts +18 -0
- package/src/transport/session/clone-session-description.ts +41 -0
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/fix-session.ts +136 -25
- package/src/transport/session/index.ts +2 -0
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-msg-factory.ts +13 -0
- package/src/transport/session/session-registry.ts +112 -0
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
- package/src/transport/tcp/tcp-acceptor.ts +92 -18
- package/src/transport/tcp/tcp-initiator.ts +26 -5
- package/src/transport/tcp/tcp-transport-description.ts +7 -0
- package/src/transport/tcp/tls-diagnostics.ts +76 -0
- package/src/transport/tcp/tls-options-factory.ts +34 -2
|
@@ -15,9 +15,9 @@ class AppLauncher extends SessionLauncher {
|
|
|
15
15
|
protected override makeFactory (config: IJsFixConfig): EngineFactory {
|
|
16
16
|
const isInitiator = this.isInitiator(config.description)
|
|
17
17
|
return {
|
|
18
|
-
makeSession: () => isInitiator
|
|
19
|
-
? new MDClient(
|
|
20
|
-
: new MDServer(
|
|
18
|
+
makeSession: (sessionConfig: IJsFixConfig) => isInitiator
|
|
19
|
+
? new MDClient(sessionConfig)
|
|
20
|
+
: new MDServer(sessionConfig)
|
|
21
21
|
} as EngineFactory
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -13,7 +13,7 @@ class AppLauncher extends SessionLauncher {
|
|
|
13
13
|
|
|
14
14
|
protected override makeFactory (config: IJsFixConfig): EngineFactory {
|
|
15
15
|
return {
|
|
16
|
-
makeSession: () => new SkeletonSession(
|
|
16
|
+
makeSession: (sessionConfig: IJsFixConfig) => new SkeletonSession(sessionConfig, 45, false)
|
|
17
17
|
} as EngineFactory
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -14,9 +14,9 @@ export class AppLauncher extends SessionLauncher {
|
|
|
14
14
|
protected override makeFactory (config: IJsFixConfig): EngineFactory {
|
|
15
15
|
const isInitiator = this.isInitiator(config.description)
|
|
16
16
|
return {
|
|
17
|
-
makeSession: () => isInitiator
|
|
18
|
-
? new TradeCaptureClient(
|
|
19
|
-
: new TradeCaptureServer(
|
|
17
|
+
makeSession: (sessionConfig: IJsFixConfig) => isInitiator
|
|
18
|
+
? new TradeCaptureClient(sessionConfig)
|
|
19
|
+
: new TradeCaptureServer(sessionConfig)
|
|
20
20
|
} as EngineFactory
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -22,7 +22,7 @@ export class FileSessionStore implements IFixSessionStore {
|
|
|
22
22
|
private creationTimeValue: Date = new Date()
|
|
23
23
|
|
|
24
24
|
// In-memory index: seqnum -> { offset, length }
|
|
25
|
-
private readonly headerIndex
|
|
25
|
+
private readonly headerIndex = new Map<number, { offset: number, length: number }>()
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Creates a FileSessionStore with the default file-based stream provider.
|
|
@@ -285,7 +285,7 @@ export class FileSessionStore implements IFixSessionStore {
|
|
|
285
285
|
*/
|
|
286
286
|
static parseSessionTime (str: string): Date | null {
|
|
287
287
|
// Format: YYYYMMDD-HH:MM:SS.ffffff
|
|
288
|
-
const match =
|
|
288
|
+
const match = /^(\d{4})(\d{2})(\d{2})-(\d{2}):(\d{2}):(\d{2})\.(\d{3,6})$/.exec(str)
|
|
289
289
|
if (!match) return null
|
|
290
290
|
const [, y, M, d, h, m, s, frac] = match
|
|
291
291
|
const ms = parseInt(frac.substring(0, 3), 10)
|
|
@@ -32,28 +32,34 @@ export class FileSessionStreamProvider implements ISessionStreamProvider {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
openBody (): void {
|
|
35
|
-
|
|
36
|
-
this.bodyFd = fs.openSync(this.bodyPath, 'a+')
|
|
37
|
-
const stat = fs.fstatSync(this.bodyFd)
|
|
38
|
-
this.bodySize = stat.size
|
|
35
|
+
this.openBodyFd()
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
/**
|
|
39
|
+
* open the body file on first use and hand back the descriptor - returning it
|
|
40
|
+
* keeps callers free of a null check the compiler cannot see through.
|
|
41
|
+
*/
|
|
42
|
+
private openBodyFd (): number {
|
|
42
43
|
if (this.bodyFd === null) {
|
|
43
|
-
this.
|
|
44
|
+
this.bodyFd = fs.openSync(this.bodyPath, 'a+')
|
|
45
|
+
const stat = fs.fstatSync(this.bodyFd)
|
|
46
|
+
this.bodySize = stat.size
|
|
44
47
|
}
|
|
48
|
+
return this.bodyFd
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async appendBody (data: Buffer): Promise<number> {
|
|
52
|
+
const fd = this.openBodyFd()
|
|
45
53
|
const offset = this.bodySize
|
|
46
|
-
fs.writeSync(
|
|
54
|
+
fs.writeSync(fd, data, 0, data.length, offset)
|
|
47
55
|
this.bodySize += data.length
|
|
48
56
|
return offset
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
async readBody (offset: number, length: number): Promise<Buffer> {
|
|
52
|
-
|
|
53
|
-
this.openBody()
|
|
54
|
-
}
|
|
60
|
+
const fd = this.openBodyFd()
|
|
55
61
|
const buf = Buffer.alloc(length)
|
|
56
|
-
fs.readSync(
|
|
62
|
+
fs.readSync(fd, buf, 0, length, offset)
|
|
57
63
|
return buf
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -7,7 +7,7 @@ import { FileSessionStore } from './file-session-store'
|
|
|
7
7
|
* Factory for creating session stores.
|
|
8
8
|
*/
|
|
9
9
|
export interface IFixSessionStoreFactory {
|
|
10
|
-
create (sessionId: SessionId)
|
|
10
|
+
create: (sessionId: SessionId) => IFixSessionStore
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -14,24 +14,24 @@ export interface IFixSessionStore {
|
|
|
14
14
|
readonly sessionId: SessionId
|
|
15
15
|
|
|
16
16
|
// Message Operations
|
|
17
|
-
put (record: IFixMsgStoreRecord)
|
|
18
|
-
get (seqNum: number)
|
|
19
|
-
getRange (fromSeqNum: number, toSeqNum: number)
|
|
17
|
+
put: (record: IFixMsgStoreRecord) => Promise<void>
|
|
18
|
+
get: (seqNum: number) => Promise<IFixMsgStoreRecord | null>
|
|
19
|
+
getRange: (fromSeqNum: number, toSeqNum: number) => Promise<IFixMsgStoreRecord[]>
|
|
20
20
|
|
|
21
21
|
// Sequence Number Operations
|
|
22
22
|
senderSeqNum: number
|
|
23
23
|
targetSeqNum: number
|
|
24
|
-
setSenderSeqNum (value: number)
|
|
25
|
-
setTargetSeqNum (value: number)
|
|
26
|
-
nextSenderSeqNum ()
|
|
27
|
-
nextTargetSeqNum ()
|
|
24
|
+
setSenderSeqNum: (value: number) => Promise<void>
|
|
25
|
+
setTargetSeqNum: (value: number) => Promise<void>
|
|
26
|
+
nextSenderSeqNum: () => Promise<number>
|
|
27
|
+
nextTargetSeqNum: () => Promise<number>
|
|
28
28
|
|
|
29
29
|
// Session Operations
|
|
30
30
|
readonly creationTime: Date
|
|
31
|
-
reset ()
|
|
31
|
+
reset: () => Promise<void>
|
|
32
32
|
|
|
33
33
|
// Lifecycle
|
|
34
|
-
initialize ()
|
|
35
|
-
flush ()
|
|
36
|
-
dispose ()
|
|
34
|
+
initialize: () => Promise<void>
|
|
35
|
+
flush: () => Promise<void>
|
|
36
|
+
dispose: () => Promise<void>
|
|
37
37
|
}
|
|
@@ -7,7 +7,7 @@ import { IFixMsgStoreRecord } from './fix-msg-store-record'
|
|
|
7
7
|
* Not persistent - all data lost on dispose.
|
|
8
8
|
*/
|
|
9
9
|
export class MemorySessionStore implements IFixSessionStore {
|
|
10
|
-
private readonly messages
|
|
10
|
+
private readonly messages = new Map<number, IFixMsgStoreRecord>()
|
|
11
11
|
private senderSeqNumValue: number = 1
|
|
12
12
|
private targetSeqNumValue: number = 1
|
|
13
13
|
private creationTimeValue: Date = new Date()
|
|
@@ -7,68 +7,68 @@ export interface ISessionStreamProvider {
|
|
|
7
7
|
* Opens or creates a read-write buffer for the message body file.
|
|
8
8
|
* Must support random-access reads (by offset+length).
|
|
9
9
|
*/
|
|
10
|
-
openBody ()
|
|
10
|
+
openBody: () => void
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Appends data to the body. Returns the offset at which data was written.
|
|
14
14
|
*/
|
|
15
|
-
appendBody (data: Buffer)
|
|
15
|
+
appendBody: (data: Buffer) => Promise<number>
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Reads data from the body at the given offset and length.
|
|
19
19
|
*/
|
|
20
|
-
readBody (offset: number, length: number)
|
|
20
|
+
readBody: (offset: number, length: number) => Promise<Buffer>
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Gets the current body size (for calculating offsets).
|
|
24
24
|
*/
|
|
25
|
-
getBodySize ()
|
|
25
|
+
getBodySize: () => number
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Appends a line to the header index file.
|
|
29
29
|
*/
|
|
30
|
-
appendHeaderLine (line: string)
|
|
30
|
+
appendHeaderLine: (line: string) => Promise<void>
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Reads all lines from the header index file.
|
|
34
34
|
* Returns empty array if no data exists.
|
|
35
35
|
*/
|
|
36
|
-
readHeaderLines ()
|
|
36
|
+
readHeaderLines: () => Promise<string[]>
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Reads the sequence numbers string.
|
|
40
40
|
* Returns null if no data exists.
|
|
41
41
|
*/
|
|
42
|
-
readSeqNums ()
|
|
42
|
+
readSeqNums: () => Promise<string | null>
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Writes the sequence numbers string.
|
|
46
46
|
*/
|
|
47
|
-
writeSeqNums (content: string)
|
|
47
|
+
writeSeqNums: (content: string) => Promise<void>
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Reads the session time string.
|
|
51
51
|
* Returns null if no data exists.
|
|
52
52
|
*/
|
|
53
|
-
readSessionTime ()
|
|
53
|
+
readSessionTime: () => Promise<string | null>
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Writes the session time string.
|
|
57
57
|
*/
|
|
58
|
-
writeSessionTime (content: string)
|
|
58
|
+
writeSessionTime: (content: string) => Promise<void>
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Resets all streams/files for a new session.
|
|
62
62
|
*/
|
|
63
|
-
reset ()
|
|
63
|
+
reset: () => Promise<void>
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Flushes any pending writes.
|
|
67
67
|
*/
|
|
68
|
-
flush ()
|
|
68
|
+
flush: () => Promise<void>
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Disposes of all resources.
|
|
72
72
|
*/
|
|
73
|
-
dispose ()
|
|
73
|
+
dispose: () => Promise<void>
|
|
74
74
|
}
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
FixMsgAsciiStoreResend, FixMsgMemoryStore, FixMsgStoreRecord,
|
|
7
7
|
IFixMsgStore, IFixMsgStoreRecord,
|
|
8
8
|
IFixSessionStore, IFixSessionStoreFactory,
|
|
9
|
-
MemorySessionStoreFactory, FileSessionStoreFactory, SessionId
|
|
9
|
+
MemorySessionStore, MemorySessionStoreFactory, FileSessionStoreFactory, SessionId
|
|
10
10
|
} from '../../store'
|
|
11
|
+
import { asMutable } from '../session/clone-session-description'
|
|
11
12
|
import { SessionState } from '../tcp'
|
|
12
13
|
import { TickAction } from '../tick-action'
|
|
13
14
|
import { IMsgApplication } from '../msg-application'
|
|
@@ -20,12 +21,24 @@ import { MsgTransport } from '../factory'
|
|
|
20
21
|
import { ILooseObject } from '../../collections/collection'
|
|
21
22
|
|
|
22
23
|
export abstract class AsciiSession extends FixSession {
|
|
24
|
+
/**
|
|
25
|
+
* TargetCompID value which tells an acceptor to take its target from whichever
|
|
26
|
+
* counterparty logs on, rather than from static configuration.
|
|
27
|
+
*/
|
|
28
|
+
public static readonly WildcardCompId = '*'
|
|
23
29
|
public heartbeat: boolean = true
|
|
24
30
|
protected store: IFixMsgStore | null = null
|
|
25
31
|
protected resender: FixMsgAsciiStoreResend
|
|
26
|
-
protected
|
|
27
|
-
protected
|
|
28
|
-
protected
|
|
32
|
+
protected coordinator: SessionSequenceCoordinator
|
|
33
|
+
protected sessionStore: IFixSessionStore
|
|
34
|
+
protected sessionId: SessionId
|
|
35
|
+
private registered: boolean = false
|
|
36
|
+
/** acceptor configured with TargetCompID '*' - identity comes from the peer Logon */
|
|
37
|
+
protected readonly wildcardTarget: boolean
|
|
38
|
+
private identityBound: boolean
|
|
39
|
+
private bindInProgress: boolean = false
|
|
40
|
+
private readonly pendingRx: Array<{ msgType: string, view: MsgView }> = []
|
|
41
|
+
private readonly clock: DefaultFixClock = new DefaultFixClock()
|
|
29
42
|
|
|
30
43
|
protected constructor (public readonly config: IJsFixConfig) {
|
|
31
44
|
super(config)
|
|
@@ -34,22 +47,38 @@ export abstract class AsciiSession extends FixSession {
|
|
|
34
47
|
this.store = new FixMsgMemoryStore(this.config.description.SenderCompId, this.config)
|
|
35
48
|
this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
|
|
36
49
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
50
|
+
// A wildcard TargetCompID lets one acceptor serve counterparties it was not
|
|
51
|
+
// statically configured for - the peer's SenderCompID becomes our target, and
|
|
52
|
+
// therefore our SessionId and store, once the Logon arrives. An initiator has
|
|
53
|
+
// to know who it is calling, so '*' is meaningless there.
|
|
54
|
+
this.wildcardTarget = config.description.TargetCompID === AsciiSession.WildcardCompId
|
|
55
|
+
if (this.wildcardTarget && this.initiator) {
|
|
56
|
+
throw new Error(`TargetCompID '${AsciiSession.WildcardCompId}' is only valid for acceptors, not initiators`)
|
|
57
|
+
}
|
|
58
|
+
this.identityBound = !this.wildcardTarget
|
|
59
|
+
|
|
40
60
|
this.sessionId = new SessionId(
|
|
41
61
|
config.description.BeginString,
|
|
42
62
|
config.description.SenderCompId,
|
|
43
63
|
config.description.TargetCompID
|
|
44
64
|
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
65
|
+
// Until the peer identifies itself there is nothing worth persisting, so a
|
|
66
|
+
// wildcard session starts on a throwaway in-memory store. The real store -
|
|
67
|
+
// keyed on the peer's comp id - is created by bindIdentity.
|
|
68
|
+
this.sessionStore = this.wildcardTarget
|
|
69
|
+
? new MemorySessionStore(this.sessionId)
|
|
70
|
+
: this.storeFactory().create(this.sessionId)
|
|
71
|
+
|
|
72
|
+
this.coordinator = new SessionSequenceCoordinator(this.sessionStore, this.clock)
|
|
49
73
|
const lastReceivedSeqNum = config.description.LastReceivedSeqNum ?? 0
|
|
50
74
|
this.coordinator.initializeFromConfig(undefined, lastReceivedSeqNum + 1)
|
|
51
75
|
}
|
|
52
76
|
|
|
77
|
+
private storeFactory (): IFixSessionStoreFactory {
|
|
78
|
+
// Priority: programmatic config > JSON store config > default in-memory
|
|
79
|
+
return this.config.sessionStoreFactory ?? AsciiSession.createStoreFactory(this.config.description.store)
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
/**
|
|
54
83
|
* Initialize the session store (reads persisted sequences from file if using FileSessionStore),
|
|
55
84
|
* sync the coordinator and transmitter from the store, then start the session.
|
|
@@ -65,6 +94,15 @@ export abstract class AsciiSession extends FixSession {
|
|
|
65
94
|
// the client's Logon can arrive and be parsed into the void.
|
|
66
95
|
transport.duplex.readable?.pause()
|
|
67
96
|
|
|
97
|
+
if (this.wildcardTarget && !this.identityBound) {
|
|
98
|
+
// Nothing to recover yet - the SessionId is not known until the peer logs on,
|
|
99
|
+
// so store initialisation, sequence sync and registry claim all happen in
|
|
100
|
+
// bindIdentity. Resume and wait for the Logon.
|
|
101
|
+
this.sessionLogger.info('wildcard acceptor: awaiting peer Logon to establish session identity')
|
|
102
|
+
transport.duplex.readable?.resume()
|
|
103
|
+
return await super.run(transport)
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
// Initialize store — reads persisted .seqnums file if using FileSessionStore.
|
|
69
107
|
// For MemorySessionStore this is a no-op.
|
|
70
108
|
await this.sessionStore.initialize()
|
|
@@ -81,6 +119,11 @@ export abstract class AsciiSession extends FixSession {
|
|
|
81
119
|
|
|
82
120
|
this.sessionLogger.info(`store initialized: nextSender=${this.coordinator.nextSenderSeqNum}, expectedTarget=${this.coordinator.expectedTargetSeqNum}`)
|
|
83
121
|
|
|
122
|
+
// Claim this SessionId. A counterparty which reconnected before its previous
|
|
123
|
+
// socket was detected as dead would otherwise leave two live sessions writing
|
|
124
|
+
// to one store - see issue #153.
|
|
125
|
+
this.claimSessionId()
|
|
126
|
+
|
|
84
127
|
// Resume the stream — super.run() will call subscribe() to hook up the message handler,
|
|
85
128
|
// then the buffered data (including the client's Logon) will be delivered.
|
|
86
129
|
transport.duplex.readable?.resume()
|
|
@@ -330,6 +373,30 @@ export abstract class AsciiSession extends FixSession {
|
|
|
330
373
|
this.sessionLogger.info('coordinator reset transient state for reconnect')
|
|
331
374
|
}
|
|
332
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Take ownership of this SessionId in the registry, stopping whatever session
|
|
378
|
+
* held it before. Idempotent - a session reused across reconnects (the
|
|
379
|
+
* RecoveringTcpInitiator pattern) simply re-registers itself.
|
|
380
|
+
*/
|
|
381
|
+
private claimSessionId (): void {
|
|
382
|
+
const registry = this.config.sessionRegistry
|
|
383
|
+
if (!registry) return
|
|
384
|
+
const stoppedOld = registry.register(this.sessionId, this)
|
|
385
|
+
if (stoppedOld) {
|
|
386
|
+
this.sessionLogger.info(`session registry stopped previous session for ${this.sessionId.toString()}`)
|
|
387
|
+
}
|
|
388
|
+
this.registered = true
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected override onSessionStopping (): void {
|
|
392
|
+
super.onSessionStopping()
|
|
393
|
+
const registry = this.config.sessionRegistry
|
|
394
|
+
if (!registry || !this.registered) return
|
|
395
|
+
this.registered = false
|
|
396
|
+
this.sessionLogger.info(`session stopping - unregistering from registry: ${this.sessionId.toString()}`)
|
|
397
|
+
registry.unregister(this.sessionId, this)
|
|
398
|
+
}
|
|
399
|
+
|
|
333
400
|
protected override async onPreLogon (): Promise<void> {
|
|
334
401
|
if (!this.config.description.ResetSeqNumFlag) return
|
|
335
402
|
|
|
@@ -477,6 +544,114 @@ export abstract class AsciiSession extends FixSession {
|
|
|
477
544
|
}
|
|
478
545
|
|
|
479
546
|
protected onMsg (msgType: string, view: MsgView): void {
|
|
547
|
+
if (this.wildcardTarget && !this.identityBound) {
|
|
548
|
+
this.onMsgBeforeIdentity(msgType, view)
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
this.dispatchMsg(msgType, view)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Message handling for a wildcard acceptor which has not yet learned who it is
|
|
556
|
+
* talking to. The first message must be a Logon; it carries the peer's
|
|
557
|
+
* SenderCompID, which becomes our TargetCompID and hence our SessionId and store.
|
|
558
|
+
*
|
|
559
|
+
* Binding is asynchronous (the store may have to be read from disk) while message
|
|
560
|
+
* delivery is not: the parser emits every message it finds in a TCP segment in one
|
|
561
|
+
* synchronous burst, and a view is only valid for the duration of its own event -
|
|
562
|
+
* AsciiParser recycles the tag index and buffer immediately afterwards. So each
|
|
563
|
+
* message that arrives mid-bind is cloned and queued, then replayed in order.
|
|
564
|
+
*/
|
|
565
|
+
private onMsgBeforeIdentity (msgType: string, view: MsgView): void {
|
|
566
|
+
if (this.bindInProgress) {
|
|
567
|
+
this.sessionLogger.info(`queueing '${msgType}' received while binding session identity`)
|
|
568
|
+
this.pendingRx.push({ msgType, view: view.clone() })
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (msgType !== MsgType.Logon) {
|
|
573
|
+
this.terminate(new Error(`wildcard acceptor expects Logon as the first message, received '${msgType}'`))
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const peerCompId = view.getString(MsgTag.SenderCompID)
|
|
578
|
+
if (!peerCompId) {
|
|
579
|
+
this.terminate(new Error('Logon has no SenderCompID - cannot establish session identity'))
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.bindInProgress = true
|
|
584
|
+
const logon = view.clone()
|
|
585
|
+
this.transport?.duplex.readable?.pause()
|
|
586
|
+
|
|
587
|
+
this.bindIdentity(peerCompId).then(() => {
|
|
588
|
+
this.bindInProgress = false
|
|
589
|
+
this.identityBound = true
|
|
590
|
+
this.dispatchMsg(MsgType.Logon, logon)
|
|
591
|
+
this.drainPendingRx()
|
|
592
|
+
this.transport?.duplex.readable?.resume()
|
|
593
|
+
}).catch((e: Error) => {
|
|
594
|
+
this.bindInProgress = false
|
|
595
|
+
this.pendingRx.length = 0
|
|
596
|
+
this.terminate(new Error(`failed to bind session identity for peer '${peerCompId}': ${e.message}`))
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private drainPendingRx (): void {
|
|
601
|
+
while (this.pendingRx.length > 0) {
|
|
602
|
+
const queued = this.pendingRx.shift()
|
|
603
|
+
if (!queued) break
|
|
604
|
+
if (this.sessionState.state === SessionState.Stopped) {
|
|
605
|
+
this.sessionLogger.info(`dropping queued '${queued.msgType}' - session stopped during bind`)
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
this.dispatchMsg(queued.msgType, queued.view)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Adopt the peer's comp id as our TargetCompID, then build the identity derived
|
|
614
|
+
* state: SessionId, message store, sequence coordinator, registry claim. This is
|
|
615
|
+
* the wildcard equivalent of the work run() does up front for a statically
|
|
616
|
+
* configured session.
|
|
617
|
+
*/
|
|
618
|
+
private async bindIdentity (peerCompId: string): Promise<void> {
|
|
619
|
+
const logger = this.sessionLogger
|
|
620
|
+
logger.info(`wildcard acceptor: binding session identity to peer SenderCompID '${peerCompId}'`)
|
|
621
|
+
|
|
622
|
+
// the description is this session's own clone (see makeSessionScope), and the
|
|
623
|
+
// session message factory holds the same object, so outbound headers pick the
|
|
624
|
+
// new target up immediately
|
|
625
|
+
asMutable(this.config.description).TargetCompID = peerCompId
|
|
626
|
+
|
|
627
|
+
// every session on a wildcard listener shared one logger name until now, which
|
|
628
|
+
// makes a multi-client server log impossible to follow
|
|
629
|
+
this.sessionLogger = this.config.logFactory.logger(`${this.me}:${peerCompId}:FixSession`)
|
|
630
|
+
|
|
631
|
+
this.sessionId = new SessionId(
|
|
632
|
+
this.config.description.BeginString,
|
|
633
|
+
this.config.description.SenderCompId,
|
|
634
|
+
peerCompId
|
|
635
|
+
)
|
|
636
|
+
this.sessionStore = this.storeFactory().create(this.sessionId)
|
|
637
|
+
await this.sessionStore.initialize()
|
|
638
|
+
|
|
639
|
+
this.coordinator = new SessionSequenceCoordinator(this.sessionStore, this.clock)
|
|
640
|
+
this.coordinator.initializeFromStore()
|
|
641
|
+
|
|
642
|
+
const transmitter = this.transport?.transmitter as AsciiMsgTransmitter | undefined
|
|
643
|
+
if (transmitter) {
|
|
644
|
+
transmitter.msgSeqNum = this.coordinator.nextSenderSeqNum
|
|
645
|
+
}
|
|
646
|
+
this.sessionState.lastPeerMsgSeqNum = this.coordinator.lastProcessedPeerSeqNum
|
|
647
|
+
|
|
648
|
+
logger.info(`session identity bound: ${this.sessionId.toString()} ` +
|
|
649
|
+
`nextSender=${this.coordinator.nextSenderSeqNum}, expectedTarget=${this.coordinator.expectedTargetSeqNum}`)
|
|
650
|
+
|
|
651
|
+
this.claimSessionId()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private dispatchMsg (msgType: string, view: MsgView): void {
|
|
480
655
|
if (!this.checkSeqNo(msgType, view)) {
|
|
481
656
|
this.sessionLogger.info(`message '${msgType}' failed checkSeqNo.`)
|
|
482
657
|
return
|
|
@@ -5,5 +5,14 @@ export type MakeDuplex = () => FixDuplex
|
|
|
5
5
|
export abstract class FixDuplex {
|
|
6
6
|
public readable: Readable
|
|
7
7
|
public writable: Writable
|
|
8
|
+
/** graceful shutdown - for a socket this is a FIN, which the peer may never answer */
|
|
8
9
|
public abstract end (): void
|
|
10
|
+
/**
|
|
11
|
+
* Release the underlying resource unconditionally. A half open socket - one whose
|
|
12
|
+
* peer has vanished without FIN or RST - will not complete a graceful end, so the
|
|
13
|
+
* acceptor needs a way to stop holding it (issue #153). Defaults to end().
|
|
14
|
+
*/
|
|
15
|
+
public destroy (): void {
|
|
16
|
+
this.end()
|
|
17
|
+
}
|
|
9
18
|
}
|
|
@@ -14,4 +14,14 @@ export class TcpDuplex extends FixDuplex {
|
|
|
14
14
|
end (): void {
|
|
15
15
|
this.socket.end()
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A socket whose peer has gone without FIN or RST will never complete the
|
|
20
|
+
* graceful end() above - the FIN is sent and no answer ever comes, so the handle
|
|
21
|
+
* stays open indefinitely. destroy() drops it regardless.
|
|
22
|
+
*/
|
|
23
|
+
destroy (): void {
|
|
24
|
+
if (this.socket.destroyed) return
|
|
25
|
+
this.socket.destroy()
|
|
26
|
+
}
|
|
17
27
|
}
|
|
@@ -34,10 +34,30 @@ export class MsgTransport {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Milliseconds to wait after a graceful end before dropping the connection. A
|
|
39
|
+
* half open socket never answers the FIN, so without this the handle - and the
|
|
40
|
+
* memory behind its session - is held for the life of the process (issue #153).
|
|
41
|
+
*/
|
|
42
|
+
public static lingerMs: number = 5000
|
|
43
|
+
|
|
37
44
|
public end (): void {
|
|
45
|
+
if (this.ended) return
|
|
46
|
+
this.ended = true
|
|
38
47
|
this.duplex.end()
|
|
48
|
+
if (MsgTransport.lingerMs <= 0) {
|
|
49
|
+
this.duplex.destroy()
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const linger = setTimeout(() => {
|
|
53
|
+
this.duplex.destroy()
|
|
54
|
+
}, MsgTransport.lingerMs)
|
|
55
|
+
// never keep the process alive purely to time out a dead socket
|
|
56
|
+
linger.unref?.()
|
|
39
57
|
}
|
|
40
58
|
|
|
59
|
+
private ended: boolean = false
|
|
60
|
+
|
|
41
61
|
public async wait (): Promise<any> {
|
|
42
62
|
return await new Promise<any>((resolve, reject) => {
|
|
43
63
|
this.receiver.on('end', () => {
|
|
@@ -20,6 +20,24 @@ export abstract class ASessionMsgFactory implements ISessionMsgFactory {
|
|
|
20
20
|
protected constructor (public readonly description: ISessionDescription, public mutator: ObjectMutator | null = null) {
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Rebuild this factory against another session description, preserving the
|
|
25
|
+
* concrete type. An acceptor needs one factory per accepted connection so that
|
|
26
|
+
* outbound headers carry that session's comp ids; without preserving the type, an
|
|
27
|
+
* application's own factory would be silently replaced by the stock one.
|
|
28
|
+
*
|
|
29
|
+
* Reconstructing through `this.constructor` covers any subclass whose constructor
|
|
30
|
+
* takes (description, mutator) - the shape every factory in this library uses.
|
|
31
|
+
* Override this if yours takes anything else.
|
|
32
|
+
*/
|
|
33
|
+
public cloneFor (description: ISessionDescription): ISessionMsgFactory {
|
|
34
|
+
const Ctor = this.constructor as new (
|
|
35
|
+
description: ISessionDescription,
|
|
36
|
+
mutator: ObjectMutator | null
|
|
37
|
+
) => ISessionMsgFactory
|
|
38
|
+
return new Ctor(description, this.mutator)
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
public reject (msgType: string, seqNo: number, msg: string, reason: number): ILooseObject {
|
|
24
42
|
const o: IReject = {
|
|
25
43
|
RefMsgType: msgType,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IDynamicSessionParams, ISessionDescription } from './session-description'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A session description with the readonly modifiers stripped. The description is
|
|
5
|
+
* immutable by design - it is loaded once from JSON and shared by everything in the
|
|
6
|
+
* session - but an acceptor running in wildcard mode has to write the peer's
|
|
7
|
+
* SenderCompID into its own copy once the Logon arrives. Cast through this type so
|
|
8
|
+
* the (deliberately rare) mutation is explicit at every call site.
|
|
9
|
+
*/
|
|
10
|
+
export type MutableSessionDescription = {
|
|
11
|
+
-readonly [K in keyof ISessionDescription]: ISessionDescription[K]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shallow clone a session description, optionally overriding identity fields.
|
|
16
|
+
*
|
|
17
|
+
* Each connection accepted by a FIX acceptor needs its own description: the
|
|
18
|
+
* SessionId (and therefore the message store), the outbound header comp ids and the
|
|
19
|
+
* session registry key are all derived from it. Sharing one description across
|
|
20
|
+
* concurrent sessions makes every client collide on the same store.
|
|
21
|
+
*
|
|
22
|
+
* `application` and `store` are shared by reference - they are read only
|
|
23
|
+
* configuration (host, port, dictionary, store directory) common to every session
|
|
24
|
+
* on the listener.
|
|
25
|
+
*/
|
|
26
|
+
export function cloneSessionDescription (
|
|
27
|
+
description: ISessionDescription,
|
|
28
|
+
overrides?: Partial<IDynamicSessionParams>): ISessionDescription {
|
|
29
|
+
return {
|
|
30
|
+
...description,
|
|
31
|
+
...(overrides ?? {})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Make a description writable. Use only where the FIX protocol itself forces a
|
|
37
|
+
* late binding - i.e. wildcard TargetCompID resolved from the peer's Logon.
|
|
38
|
+
*/
|
|
39
|
+
export function asMutable (description: ISessionDescription): MutableSessionDescription {
|
|
40
|
+
return description as MutableSessionDescription
|
|
41
|
+
}
|