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.
Files changed (180) hide show
  1. package/.gitattributes +4 -0
  2. package/BACKPORT_PLAN.md +84 -0
  3. package/DEMO_PORT_PLAN.md +10 -0
  4. package/README.md +3 -0
  5. package/WORKLOG.md +119 -0
  6. package/dist/benchmark/bench-report.d.ts +5 -0
  7. package/dist/benchmark/bench-report.js +91 -0
  8. package/dist/benchmark/bench-report.js.map +1 -0
  9. package/dist/benchmark/bench-runner.d.ts +12 -0
  10. package/dist/benchmark/bench-runner.js +193 -0
  11. package/dist/benchmark/bench-runner.js.map +1 -0
  12. package/dist/benchmark/bench-stats.d.ts +10 -0
  13. package/dist/benchmark/bench-stats.js +56 -0
  14. package/dist/benchmark/bench-stats.js.map +1 -0
  15. package/dist/benchmark/bench-types.d.ts +74 -0
  16. package/dist/benchmark/bench-types.js +3 -0
  17. package/dist/benchmark/bench-types.js.map +1 -0
  18. package/dist/benchmark/gc-probe.d.ts +18 -0
  19. package/dist/benchmark/gc-probe.js +103 -0
  20. package/dist/benchmark/gc-probe.js.map +1 -0
  21. package/dist/benchmark/index.d.ts +6 -0
  22. package/dist/benchmark/index.js +23 -0
  23. package/dist/benchmark/index.js.map +1 -0
  24. package/dist/benchmark/parse-bench.d.ts +28 -0
  25. package/dist/benchmark/parse-bench.js +85 -0
  26. package/dist/benchmark/parse-bench.js.map +1 -0
  27. package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
  28. package/dist/buffer/ascii/ascii-parser.js +4 -1
  29. package/dist/buffer/ascii/ascii-parser.js.map +1 -1
  30. package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
  31. package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
  32. package/dist/buffer/ascii/tag-index.js.map +1 -1
  33. package/dist/buffer/msg-view.d.ts +1 -0
  34. package/dist/buffer/msg-view.js +25 -19
  35. package/dist/buffer/msg-view.js.map +1 -1
  36. package/dist/config/js-fix-config.d.ts +4 -0
  37. package/dist/config/js-fix-config.js.map +1 -1
  38. package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
  39. package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
  40. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
  41. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
  42. package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
  43. package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
  44. package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
  45. package/dist/jsfix-bench.d.ts +1 -0
  46. package/dist/jsfix-bench.js +213 -0
  47. package/dist/jsfix-bench.js.map +1 -0
  48. package/dist/runtime/session-launcher.js +1 -1
  49. package/dist/runtime/session-launcher.js.map +1 -1
  50. package/dist/runtime/session-scope.d.ts +3 -0
  51. package/dist/runtime/session-scope.js +47 -0
  52. package/dist/runtime/session-scope.js.map +1 -0
  53. package/dist/sample/http/oms/app.js +3 -3
  54. package/dist/sample/http/oms/app.js.map +1 -1
  55. package/dist/sample/tcp/qf-md/app.js +3 -3
  56. package/dist/sample/tcp/qf-md/app.js.map +1 -1
  57. package/dist/sample/tcp/skeleton/app.js +1 -1
  58. package/dist/sample/tcp/skeleton/app.js.map +1 -1
  59. package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
  60. package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
  61. package/dist/store/file-session-store.js +1 -1
  62. package/dist/store/file-session-store.js.map +1 -1
  63. package/dist/store/file-session-stream-provider.d.ts +1 -0
  64. package/dist/store/file-session-stream-provider.js +13 -13
  65. package/dist/store/file-session-stream-provider.js.map +1 -1
  66. package/dist/store/fix-session-store-factory.d.ts +1 -1
  67. package/dist/store/fix-session-store-factory.js.map +1 -1
  68. package/dist/store/fix-session-store.d.ts +11 -11
  69. package/dist/store/fix-session-store.js.map +1 -1
  70. package/dist/store/memory-session-store.js.map +1 -1
  71. package/dist/store/session-stream-provider.d.ts +13 -13
  72. package/dist/store/session-stream-provider.js.map +1 -1
  73. package/dist/transport/ascii/ascii-session.d.ts +17 -3
  74. package/dist/transport/ascii/ascii-session.js +121 -8
  75. package/dist/transport/ascii/ascii-session.js.map +1 -1
  76. package/dist/transport/duplex/fix-duplex.d.ts +1 -0
  77. package/dist/transport/duplex/fix-duplex.js +3 -0
  78. package/dist/transport/duplex/fix-duplex.js.map +1 -1
  79. package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
  80. package/dist/transport/duplex/tcp-duplex.js +5 -0
  81. package/dist/transport/duplex/tcp-duplex.js.map +1 -1
  82. package/dist/transport/factory/msg-transport.d.ts +2 -0
  83. package/dist/transport/factory/msg-transport.js +17 -2
  84. package/dist/transport/factory/msg-transport.js.map +1 -1
  85. package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
  86. package/dist/transport/session/a-session-msg-factory.js +4 -0
  87. package/dist/transport/session/a-session-msg-factory.js.map +1 -1
  88. package/dist/transport/session/clone-session-description.d.ts +6 -0
  89. package/dist/transport/session/clone-session-description.js +11 -0
  90. package/dist/transport/session/clone-session-description.js.map +1 -0
  91. package/dist/transport/session/fix-clock.d.ts +1 -1
  92. package/dist/transport/session/fix-clock.js.map +1 -1
  93. package/dist/transport/session/fix-session.d.ts +5 -1
  94. package/dist/transport/session/fix-session.js +93 -24
  95. package/dist/transport/session/fix-session.js.map +1 -1
  96. package/dist/transport/session/index.d.ts +2 -0
  97. package/dist/transport/session/index.js +2 -0
  98. package/dist/transport/session/index.js.map +1 -1
  99. package/dist/transport/session/resend-request-manager.d.ts +2 -2
  100. package/dist/transport/session/resend-request-manager.js.map +1 -1
  101. package/dist/transport/session/session-msg-factory.d.ts +1 -0
  102. package/dist/transport/session/session-msg-factory.js.map +1 -1
  103. package/dist/transport/session/session-registry.d.ts +23 -0
  104. package/dist/transport/session/session-registry.js +59 -0
  105. package/dist/transport/session/session-registry.js.map +1 -0
  106. package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
  107. package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
  108. package/dist/transport/session/session-sequence-store.d.ts +3 -3
  109. package/dist/transport/session/session-sequence-store.js.map +1 -1
  110. package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
  111. package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
  112. package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
  113. package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
  114. package/dist/transport/tcp/tcp-acceptor.js +74 -20
  115. package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
  116. package/dist/transport/tcp/tcp-initiator.js +15 -5
  117. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  118. package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
  119. package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
  120. package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
  121. package/dist/transport/tcp/tls-diagnostics.js +64 -0
  122. package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
  123. package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
  124. package/dist/transport/tcp/tls-options-factory.js +24 -2
  125. package/dist/transport/tcp/tls-options-factory.js.map +1 -1
  126. package/docs/acceptor.md +207 -0
  127. package/docs/benchmarking.md +117 -0
  128. package/eslint.config.mjs +15 -1
  129. package/jsfix.test_client.txt +62 -75
  130. package/jsfix.test_server.txt +72 -72
  131. package/package.json +19 -10
  132. package/script/pack-local.js +68 -0
  133. package/src/benchmark/bench-report.ts +101 -0
  134. package/src/benchmark/bench-runner.ts +196 -0
  135. package/src/benchmark/bench-stats.ts +86 -0
  136. package/src/benchmark/bench-types.ts +110 -0
  137. package/src/benchmark/gc-probe.ts +145 -0
  138. package/src/benchmark/index.ts +6 -0
  139. package/src/benchmark/parse-bench.ts +144 -0
  140. package/src/buffer/ascii/ascii-parser.ts +16 -1
  141. package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
  142. package/src/buffer/ascii/tag-index.ts +3 -3
  143. package/src/buffer/msg-view.ts +35 -18
  144. package/src/config/js-fix-config.ts +8 -0
  145. package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
  146. package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
  147. package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
  148. package/src/jsfix-bench.ts +204 -0
  149. package/src/runtime/session-launcher.ts +5 -1
  150. package/src/runtime/session-scope.ts +84 -0
  151. package/src/sample/http/oms/app.ts +3 -3
  152. package/src/sample/tcp/qf-md/app.ts +3 -3
  153. package/src/sample/tcp/skeleton/app.ts +1 -1
  154. package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
  155. package/src/store/file-session-store.ts +2 -2
  156. package/src/store/file-session-stream-provider.ts +17 -11
  157. package/src/store/fix-session-store-factory.ts +1 -1
  158. package/src/store/fix-session-store.ts +11 -11
  159. package/src/store/memory-session-store.ts +1 -1
  160. package/src/store/session-stream-provider.ts +13 -13
  161. package/src/transport/ascii/ascii-session.ts +186 -11
  162. package/src/transport/duplex/fix-duplex.ts +9 -0
  163. package/src/transport/duplex/tcp-duplex.ts +10 -0
  164. package/src/transport/factory/msg-transport.ts +20 -0
  165. package/src/transport/session/a-session-msg-factory.ts +18 -0
  166. package/src/transport/session/clone-session-description.ts +41 -0
  167. package/src/transport/session/fix-clock.ts +1 -1
  168. package/src/transport/session/fix-session.ts +136 -25
  169. package/src/transport/session/index.ts +2 -0
  170. package/src/transport/session/resend-request-manager.ts +4 -4
  171. package/src/transport/session/session-msg-factory.ts +13 -0
  172. package/src/transport/session/session-registry.ts +112 -0
  173. package/src/transport/session/session-sequence-coordinator.ts +1 -1
  174. package/src/transport/session/session-sequence-store.ts +3 -3
  175. package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
  176. package/src/transport/tcp/tcp-acceptor.ts +92 -18
  177. package/src/transport/tcp/tcp-initiator.ts +26 -5
  178. package/src/transport/tcp/tcp-transport-description.ts +7 -0
  179. package/src/transport/tcp/tls-diagnostics.ts +76 -0
  180. 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(config)
20
- : new MDServer(config)
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(config, 45, false)
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(config)
19
- : new TradeCaptureServer(config)
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: Map<number, { offset: number, length: number }> = new Map()
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 = str.match(/^(\d{4})(\d{2})(\d{2})-(\d{2}):(\d{2}):(\d{2})\.(\d{3,6})$/)
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
- if (this.bodyFd !== null) return
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
- async appendBody (data: Buffer): Promise<number> {
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.openBody()
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(this.bodyFd!, data, 0, data.length, offset)
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
- if (this.bodyFd === null) {
53
- this.openBody()
54
- }
60
+ const fd = this.openBodyFd()
55
61
  const buf = Buffer.alloc(length)
56
- fs.readSync(this.bodyFd!, buf, 0, length, offset)
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): IFixSessionStore
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): Promise<void>
18
- get (seqNum: number): Promise<IFixMsgStoreRecord | null>
19
- getRange (fromSeqNum: number, toSeqNum: number): Promise<IFixMsgStoreRecord[]>
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): Promise<void>
25
- setTargetSeqNum (value: number): Promise<void>
26
- nextSenderSeqNum (): Promise<number>
27
- nextTargetSeqNum (): Promise<number>
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 (): Promise<void>
31
+ reset: () => Promise<void>
32
32
 
33
33
  // Lifecycle
34
- initialize (): Promise<void>
35
- flush (): Promise<void>
36
- dispose (): Promise<void>
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: Map<number, IFixMsgStoreRecord> = new Map()
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 (): void
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): Promise<number>
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): Promise<Buffer>
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 (): number
25
+ getBodySize: () => number
26
26
 
27
27
  /**
28
28
  * Appends a line to the header index file.
29
29
  */
30
- appendHeaderLine (line: string): Promise<void>
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 (): Promise<string[]>
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 (): Promise<string | null>
42
+ readSeqNums: () => Promise<string | null>
43
43
 
44
44
  /**
45
45
  * Writes the sequence numbers string.
46
46
  */
47
- writeSeqNums (content: string): Promise<void>
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 (): Promise<string | null>
53
+ readSessionTime: () => Promise<string | null>
54
54
 
55
55
  /**
56
56
  * Writes the session time string.
57
57
  */
58
- writeSessionTime (content: string): Promise<void>
58
+ writeSessionTime: (content: string) => Promise<void>
59
59
 
60
60
  /**
61
61
  * Resets all streams/files for a new session.
62
62
  */
63
- reset (): Promise<void>
63
+ reset: () => Promise<void>
64
64
 
65
65
  /**
66
66
  * Flushes any pending writes.
67
67
  */
68
- flush (): Promise<void>
68
+ flush: () => Promise<void>
69
69
 
70
70
  /**
71
71
  * Disposes of all resources.
72
72
  */
73
- dispose (): Promise<void>
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 readonly coordinator: SessionSequenceCoordinator
27
- protected readonly sessionStore: IFixSessionStore
28
- protected readonly sessionId: SessionId
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
- // Create session store from factory.
38
- // Priority: programmatic config > JSON store config > default in-memory
39
- const storeFactory = config.sessionStoreFactory ?? AsciiSession.createStoreFactory(config.description.store)
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
- this.sessionStore = storeFactory.create(this.sessionId)
46
-
47
- const clock = new DefaultFixClock()
48
- this.coordinator = new SessionSequenceCoordinator(this.sessionStore, clock)
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
+ }
@@ -1,5 +1,5 @@
1
1
  export interface IFixClock {
2
- now (): Date
2
+ now: () => Date
3
3
  }
4
4
 
5
5
  export class DefaultFixClock implements IFixClock {