jspurefix 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/BACKPORT_PLAN.md +138 -79
  2. package/dist/buffer/fixml/fixml-view.js.map +1 -1
  3. package/dist/buffer/msg-encoder.js +34 -1
  4. package/dist/buffer/msg-encoder.js.map +1 -1
  5. package/dist/buffer/msg-parser.js +34 -1
  6. package/dist/buffer/msg-parser.js.map +1 -1
  7. package/dist/buffer/msg-view.js.map +1 -1
  8. package/dist/collections/index.js +1 -0
  9. package/dist/config/js-fix-config.d.ts +2 -0
  10. package/dist/config/js-fix-config.js.map +1 -1
  11. package/dist/config/winston-logger.js.map +1 -1
  12. package/dist/dict-parser.js +34 -1
  13. package/dist/dict-parser.js.map +1 -1
  14. package/dist/dictionary/compiler/enum-compiler.js +37 -4
  15. package/dist/dictionary/compiler/enum-compiler.js.map +1 -1
  16. package/dist/dictionary/compiler/msg-compiler.js +36 -3
  17. package/dist/dictionary/compiler/msg-compiler.js.map +1 -1
  18. package/dist/dictionary/compiler/standard-snippet.js +34 -1
  19. package/dist/dictionary/compiler/standard-snippet.js.map +1 -1
  20. package/dist/dictionary/contained/contained-field-set.js +2 -0
  21. package/dist/dictionary/contained/contained-field-set.js.map +1 -1
  22. package/dist/dictionary/definition/simple-field-definition.js +34 -1
  23. package/dist/dictionary/definition/simple-field-definition.js.map +1 -1
  24. package/dist/dictionary/fix-parser.js +34 -1
  25. package/dist/dictionary/fix-parser.js.map +1 -1
  26. package/dist/dictionary/parser/fix-repository/repository-type.js +1 -0
  27. package/dist/dictionary/parser/fix-repository/repository-xml-parser.js +35 -2
  28. package/dist/dictionary/parser/fix-repository/repository-xml-parser.js.map +1 -1
  29. package/dist/dictionary/parser/fixml/fields-parser.js.map +1 -1
  30. package/dist/dictionary/parser/fixml/fix-xsd-parser.js +34 -1
  31. package/dist/dictionary/parser/fixml/fix-xsd-parser.js.map +1 -1
  32. package/dist/dictionary/parser/fixml/include-graph.js +35 -2
  33. package/dist/dictionary/parser/fixml/include-graph.js.map +1 -1
  34. package/dist/dictionary/parser/fixml/node-definitions.js +1 -0
  35. package/dist/dictionary/parser/fixml/xsd-parser.js +34 -1
  36. package/dist/dictionary/parser/fixml/xsd-parser.js.map +1 -1
  37. package/dist/jsfix-cmd.js +39 -3
  38. package/dist/jsfix-cmd.js.map +1 -1
  39. package/dist/runtime/session-launcher.js +34 -1
  40. package/dist/runtime/session-launcher.js.map +1 -1
  41. package/dist/sample/http/oms/app.js +34 -1
  42. package/dist/sample/http/oms/app.js.map +1 -1
  43. package/dist/sample/tcp/recovering-skeleton/app.js +34 -1
  44. package/dist/sample/tcp/recovering-skeleton/app.js.map +1 -1
  45. package/dist/store/file-session-store.d.ts +42 -0
  46. package/dist/store/file-session-store.js +256 -0
  47. package/dist/store/file-session-store.js.map +1 -0
  48. package/dist/store/file-session-stream-provider.d.ts +25 -0
  49. package/dist/store/file-session-stream-provider.js +162 -0
  50. package/dist/store/file-session-stream-provider.js.map +1 -0
  51. package/dist/store/fix-msg-ascii-store-resend.js +1 -1
  52. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  53. package/dist/store/fix-session-store-factory.d.ts +13 -0
  54. package/dist/store/fix-session-store-factory.js +21 -0
  55. package/dist/store/fix-session-store-factory.js.map +1 -0
  56. package/dist/store/fix-session-store.d.ts +19 -0
  57. package/dist/store/fix-session-store.js +3 -0
  58. package/dist/store/fix-session-store.js.map +1 -0
  59. package/dist/store/index.d.ts +9 -0
  60. package/dist/store/index.js +9 -0
  61. package/dist/store/index.js.map +1 -1
  62. package/dist/store/memory-session-store.d.ts +27 -0
  63. package/dist/store/memory-session-store.js +104 -0
  64. package/dist/store/memory-session-store.js.map +1 -0
  65. package/dist/store/memory-session-stream-provider.d.ts +26 -0
  66. package/dist/store/memory-session-stream-provider.js +103 -0
  67. package/dist/store/memory-session-stream-provider.js.map +1 -0
  68. package/dist/store/session-id.d.ts +9 -0
  69. package/dist/store/session-id.js +55 -0
  70. package/dist/store/session-id.js.map +1 -0
  71. package/dist/store/session-stream-provider.d.ts +15 -0
  72. package/dist/store/session-stream-provider.js +3 -0
  73. package/dist/store/session-stream-provider.js.map +1 -0
  74. package/dist/store/store-config.d.ts +4 -0
  75. package/dist/store/store-config.js +3 -0
  76. package/dist/store/store-config.js.map +1 -0
  77. package/dist/transport/ascii/ascii-session.d.ts +12 -1
  78. package/dist/transport/ascii/ascii-session.js +154 -5
  79. package/dist/transport/ascii/ascii-session.js.map +1 -1
  80. package/dist/transport/duplex/http-duplex.js +4 -1
  81. package/dist/transport/duplex/http-duplex.js.map +1 -1
  82. package/dist/transport/duplex/tcp-duplex.js +34 -1
  83. package/dist/transport/duplex/tcp-duplex.js.map +1 -1
  84. package/dist/transport/fix-acceptor.js +34 -1
  85. package/dist/transport/fix-acceptor.js.map +1 -1
  86. package/dist/transport/fix-entity.js +34 -1
  87. package/dist/transport/fix-entity.js.map +1 -1
  88. package/dist/transport/fixml/fixml-msg-transmitter.js +1 -1
  89. package/dist/transport/fixml/fixml-msg-transmitter.js.map +1 -1
  90. package/dist/transport/http/http-acceptor.js +34 -1
  91. package/dist/transport/http/http-acceptor.js.map +1 -1
  92. package/dist/transport/msg-transmitter.js +34 -1
  93. package/dist/transport/msg-transmitter.js.map +1 -1
  94. package/dist/transport/session/a-session-msg-factory.d.ts +1 -1
  95. package/dist/transport/session/a-session-msg-factory.js.map +1 -1
  96. package/dist/transport/session/fix-clock.d.ts +6 -0
  97. package/dist/transport/session/fix-clock.js +10 -0
  98. package/dist/transport/session/fix-clock.js.map +1 -0
  99. package/dist/transport/session/fix-session.d.ts +1 -0
  100. package/dist/transport/session/fix-session.js +37 -1
  101. package/dist/transport/session/fix-session.js.map +1 -1
  102. package/dist/transport/session/index.d.ts +4 -0
  103. package/dist/transport/session/index.js +4 -0
  104. package/dist/transport/session/index.js.map +1 -1
  105. package/dist/transport/session/resend-request-manager.d.ts +69 -0
  106. package/dist/transport/session/resend-request-manager.js +208 -0
  107. package/dist/transport/session/resend-request-manager.js.map +1 -0
  108. package/dist/transport/session/session-description.d.ts +2 -0
  109. package/dist/transport/session/session-description.js.map +1 -1
  110. package/dist/transport/session/session-msg-factory.d.ts +1 -1
  111. package/dist/transport/session/session-msg-factory.js.map +1 -1
  112. package/dist/transport/session/session-sequence-coordinator.d.ts +38 -0
  113. package/dist/transport/session/session-sequence-coordinator.js +180 -0
  114. package/dist/transport/session/session-sequence-coordinator.js.map +1 -0
  115. package/dist/transport/session/session-sequence-store.d.ts +14 -0
  116. package/dist/transport/session/session-sequence-store.js +36 -0
  117. package/dist/transport/session/session-sequence-store.js.map +1 -0
  118. package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
  119. package/dist/transport/tcp/tcp-initiator.js +34 -1
  120. package/dist/transport/tcp/tcp-initiator.js.map +1 -1
  121. package/dist/types/FIX4.4/index.js +1 -0
  122. package/dist/util/buffer-helper.js +34 -1
  123. package/dist/util/buffer-helper.js.map +1 -1
  124. package/dist/util/definition-factory.js +35 -2
  125. package/dist/util/definition-factory.js.map +1 -1
  126. package/jsfix.test_client.txt +67 -66
  127. package/jsfix.test_server.txt +64 -63
  128. package/package.json +11 -10
  129. package/src/buffer/fixml/fixml-view.ts +1 -1
  130. package/src/buffer/msg-view.ts +1 -1
  131. package/src/config/js-fix-config.ts +2 -0
  132. package/src/config/winston-logger.ts +3 -3
  133. package/src/dictionary/contained/contained-field-set.ts +2 -1
  134. package/src/dictionary/parser/fixml/fields-parser.ts +2 -2
  135. package/src/jsfix-cmd.ts +1 -1
  136. package/src/store/file-session-store.ts +294 -0
  137. package/src/store/file-session-stream-provider.ts +123 -0
  138. package/src/store/fix-msg-ascii-store-resend.ts +1 -1
  139. package/src/store/fix-session-store-factory.ts +31 -0
  140. package/src/store/fix-session-store.ts +37 -0
  141. package/src/store/index.ts +9 -0
  142. package/src/store/memory-session-store.ts +102 -0
  143. package/src/store/memory-session-stream-provider.ts +97 -0
  144. package/src/store/session-id.ts +32 -0
  145. package/src/store/session-stream-provider.ts +74 -0
  146. package/src/store/store-config.ts +15 -0
  147. package/src/transport/ascii/ascii-session.ts +218 -6
  148. package/src/transport/fixml/fixml-msg-transmitter.ts +1 -1
  149. package/src/transport/http/http-acceptor.ts +1 -1
  150. package/src/transport/session/a-session-msg-factory.ts +1 -1
  151. package/src/transport/session/fix-clock.ts +9 -0
  152. package/src/transport/session/fix-session.ts +5 -0
  153. package/src/transport/session/index.ts +4 -0
  154. package/src/transport/session/resend-request-manager.ts +268 -0
  155. package/src/transport/session/session-description.ts +2 -0
  156. package/src/transport/session/session-msg-factory.ts +1 -1
  157. package/src/transport/session/session-sequence-coordinator.ts +272 -0
  158. package/src/transport/session/session-sequence-store.ts +33 -0
  159. package/src/transport/tcp/tcp-acceptor.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspurefix",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "pure node js fix engine",
5
5
  "keywords": [
6
6
  "typescript",
@@ -74,42 +74,43 @@
74
74
  "author": "",
75
75
  "license": "MIT",
76
76
  "dependencies": {
77
+ "@jest/globals": "^30.3.0",
77
78
  "align-text": "^1.0.2",
78
- "axios": "^1.13.2",
79
+ "axios": "^1.14.0",
79
80
  "express": "^5.2.1",
80
81
  "lodash": "^4.17.23",
81
- "mathjs": "^15.1.0",
82
+ "mathjs": "^15.1.1",
82
83
  "minimist": "^1.2.8",
83
84
  "minimist-options": "^4.1.0",
84
85
  "moment": "^2.30.1",
85
86
  "node-fs-extra": "^0.8.2",
86
87
  "reflect-metadata": "^0.2.2",
87
- "sax": "^1.4.4",
88
+ "sax": "^1.6.0",
88
89
  "tsyringe": "^4.10.0",
89
90
  "uuid": "^9.0.1",
90
91
  "winston": "^3.19.0",
91
92
  "word-wrap": "^1.2.5",
92
- "yauzl": "^3.2.0"
93
+ "yauzl": "^3.2.1"
93
94
  },
94
95
  "devDependencies": {
95
96
  "@stylistic/eslint-plugin": "^5.10.0",
96
97
  "@types/express": "^5.0.6",
97
98
  "@types/express-serve-static-core": "^5.1.1",
98
99
  "@types/jest": "^30.0.0",
99
- "@types/lodash": "^4.17.23",
100
+ "@types/lodash": "^4.17.24",
100
101
  "@types/mathjs": "^9.4.2",
101
102
  "@types/minimist": "^1.2.5",
102
- "@types/node": "^25.0.10",
103
+ "@types/node": "^25.5.0",
103
104
  "@types/request-promise-native": "^1.0.21",
104
105
  "@types/sax": "^1.2.7",
105
- "@types/uuid": "^9.0.7",
106
+ "@types/uuid": "^9.0.8",
106
107
  "@types/winston": "^2.4.4",
107
108
  "eslint": "^9.39.4",
108
109
  "eslint-config-love": "^151.0.0",
109
- "jest": "^30.2.0",
110
+ "jest": "^30.3.0",
110
111
  "madge": "^8.0.0",
111
112
  "standard": "^17.1.2",
112
113
  "ts-jest": "^29.4.6",
113
- "typescript": "^5.9.3"
114
+ "typescript": "^6.0.2"
114
115
  }
115
116
  }
@@ -3,7 +3,7 @@ import { FixDefinitions, SimpleFieldDefinition } from '../../dictionary/definiti
3
3
  import { Structure } from '../structure'
4
4
  import { SegmentDescription } from '../segment/segment-description'
5
5
  import { AsciiChars } from '../ascii/'
6
- import * as moment from 'moment'
6
+ import moment = require('moment')
7
7
  import { TagType } from '../tag/tag-type'
8
8
 
9
9
  export class FixmlView extends MsgView {
@@ -265,7 +265,7 @@ export abstract class MsgView {
265
265
 
266
266
  public getView (name: string): MsgView | null {
267
267
  const parts: string[] = name.split('.')
268
- const reducer = (a: MsgView, current: string): MsgView | null => {
268
+ const reducer = (a: MsgView | null, current: string): MsgView | null => {
269
269
  if (!a) {
270
270
  return a
271
271
  }
@@ -5,6 +5,7 @@ import { JsFixLoggerFactory } from './js-fix-logger-factory'
5
5
  import { EmptyLogFactory } from './empty-log-factory'
6
6
  import { AsciiChars } from '../buffer/ascii/ascii-chars'
7
7
  import { DependencyContainer } from 'tsyringe'
8
+ import { IFixSessionStoreFactory } from '../store/fix-session-store-factory'
8
9
 
9
10
  export interface IJsFixConfig {
10
11
  factory: ISessionMsgFactory | null
@@ -14,6 +15,7 @@ export interface IJsFixConfig {
14
15
  logDelimiter?: number
15
16
  logFactory: JsFixLoggerFactory
16
17
  sessionContainer: DependencyContainer
18
+ sessionStoreFactory?: IFixSessionStoreFactory
17
19
  }
18
20
 
19
21
  export class JsFixConfig implements IJsFixConfig {
@@ -68,13 +68,13 @@ export class WinstonLogger {
68
68
  },
69
69
 
70
70
  info: function (msg: string): void {
71
- this.log(msg)
71
+ (this as any).log(msg)
72
72
  },
73
73
  debug: function (msg: string): void {
74
- this.log(msg)
74
+ (this as any).log(msg)
75
75
  },
76
76
  warning: function (msg: string): void {
77
- this.log(msg)
77
+ (this as any).log(msg)
78
78
  },
79
79
  error: function (): void {
80
80
  // nothing
@@ -138,7 +138,8 @@ export abstract class ContainedFieldSet implements IContainedSet {
138
138
  */
139
139
  public getSet (path: string): (IContainedSet | null) {
140
140
  if (!path) return null
141
- return path.split('.').reduce((set: IContainedSet, next: string): (IContainedSet | null) => {
141
+ return path.split('.').reduce<IContainedSet | null>((set: IContainedSet | null, next: string): (IContainedSet | null) => {
142
+ if (!set) return null
142
143
  return set.groups.get(next) ?? set.components.get(next) ?? null
143
144
  }, this)
144
145
  }
@@ -93,8 +93,8 @@ export class FieldsParser extends XsdParser {
93
93
  }
94
94
 
95
95
  private insertFields (): void {
96
- const alias = this.alias
97
- this.data.forEach((f: ISimpleField) => {
96
+ const alias = this.alias;
97
+ (this.data as ISimpleField[]).forEach((f: ISimpleField) => {
98
98
  const sf: SimpleFieldDefinition = new SimpleFieldDefinition(f.Tag,
99
99
  f.name,
100
100
  f.AbbrName,
package/src/jsfix-cmd.ts CHANGED
@@ -11,7 +11,7 @@ import { MsgTag } from './types'
11
11
  import { IJsFixConfig } from './config'
12
12
 
13
13
  import * as util from 'util'
14
- import * as minimist from 'minimist'
14
+ import minimist = require('minimist')
15
15
  import * as path from 'path'
16
16
  import { MsgTransport } from './transport/factory'
17
17
  import { EnumCompiler, ICompilerSettings, MsgCompiler } from './dictionary/compiler'
@@ -0,0 +1,294 @@
1
+ import { IFixSessionStore } from './fix-session-store'
2
+ import { SessionId } from './session-id'
3
+ import { IFixMsgStoreRecord, FixMsgStoreRecord } from './fix-msg-store-record'
4
+ import { ISessionStreamProvider } from './session-stream-provider'
5
+ import { FileSessionStreamProvider } from './file-session-stream-provider'
6
+
7
+ /**
8
+ * QuickFix-compatible file-based session store.
9
+ *
10
+ * File format:
11
+ * - .seqnums: "SSSSSSSSSSSSSSSSSSSS : TTTTTTTTTTTTTTTTTTTT" (20-char right-justified sender : target)
12
+ * - .session: "YYYYMMDD-HH:MM:SS.ffffff" (session creation time)
13
+ * - .header: "seqnum,offset,length" per line (index into body)
14
+ * - .body: concatenated raw FIX messages (no delimiters)
15
+ */
16
+ export class FileSessionStore implements IFixSessionStore {
17
+ private readonly streamProvider: ISessionStreamProvider
18
+ private readonly ownsProvider: boolean
19
+
20
+ private senderSeqNumValue: number = 1
21
+ private targetSeqNumValue: number = 1
22
+ private creationTimeValue: Date = new Date()
23
+
24
+ // In-memory index: seqnum -> { offset, length }
25
+ private readonly headerIndex: Map<number, { offset: number, length: number }> = new Map()
26
+
27
+ /**
28
+ * Creates a FileSessionStore with the default file-based stream provider.
29
+ */
30
+ static createWithFiles (sessionId: SessionId, directory: string): FileSessionStore {
31
+ return new FileSessionStore(sessionId, new FileSessionStreamProvider(sessionId, directory), true)
32
+ }
33
+
34
+ /**
35
+ * Creates a FileSessionStore with a custom stream provider.
36
+ * Useful for testing with in-memory streams.
37
+ */
38
+ constructor (
39
+ public readonly sessionId: SessionId,
40
+ streamProvider: ISessionStreamProvider,
41
+ ownsProvider: boolean = false
42
+ ) {
43
+ this.streamProvider = streamProvider
44
+ this.ownsProvider = ownsProvider
45
+ }
46
+
47
+ /**
48
+ * Gets the stream provider for direct access (useful for testing).
49
+ */
50
+ getStreamProvider (): ISessionStreamProvider {
51
+ return this.streamProvider
52
+ }
53
+
54
+ // Sequence Numbers
55
+
56
+ get senderSeqNum (): number {
57
+ return this.senderSeqNumValue
58
+ }
59
+
60
+ set senderSeqNum (value: number) {
61
+ this.senderSeqNumValue = value
62
+ }
63
+
64
+ get targetSeqNum (): number {
65
+ return this.targetSeqNumValue
66
+ }
67
+
68
+ set targetSeqNum (value: number) {
69
+ this.targetSeqNumValue = value
70
+ }
71
+
72
+ async setSenderSeqNum (value: number): Promise<void> {
73
+ this.senderSeqNumValue = value
74
+ await this.persistSeqNums()
75
+ }
76
+
77
+ async setTargetSeqNum (value: number): Promise<void> {
78
+ this.targetSeqNumValue = value
79
+ await this.persistSeqNums()
80
+ }
81
+
82
+ async nextSenderSeqNum (): Promise<number> {
83
+ const next = ++this.senderSeqNumValue
84
+ await this.persistSeqNums()
85
+ return next
86
+ }
87
+
88
+ async nextTargetSeqNum (): Promise<number> {
89
+ const next = ++this.targetSeqNumValue
90
+ await this.persistSeqNums()
91
+ return next
92
+ }
93
+
94
+ // Session
95
+
96
+ get creationTime (): Date {
97
+ return this.creationTimeValue
98
+ }
99
+
100
+ async reset (): Promise<void> {
101
+ this.senderSeqNumValue = 1
102
+ this.targetSeqNumValue = 1
103
+ this.creationTimeValue = new Date()
104
+ this.headerIndex.clear()
105
+
106
+ await this.streamProvider.reset()
107
+ await this.persistSeqNums()
108
+ await this.persistSessionTime()
109
+ this.streamProvider.openBody()
110
+ }
111
+
112
+ // Message Operations
113
+
114
+ async put (record: IFixMsgStoreRecord): Promise<void> {
115
+ if (record.encoded == null) {
116
+ throw new Error('Record must have encoded content')
117
+ }
118
+
119
+ const bytes = Buffer.from(record.encoded, 'utf8')
120
+ const length = bytes.length
121
+ const offset = await this.streamProvider.appendBody(bytes)
122
+ this.headerIndex.set(record.seqNum, { offset, length })
123
+ await this.streamProvider.appendHeaderLine(`${record.seqNum},${offset},${length}`)
124
+ }
125
+
126
+ async get (seqNum: number): Promise<IFixMsgStoreRecord | null> {
127
+ const entry = this.headerIndex.get(seqNum)
128
+ if (!entry) return null
129
+ return await this.readMessage(seqNum, entry.offset, entry.length)
130
+ }
131
+
132
+ async getRange (fromSeqNum: number, toSeqNum: number): Promise<IFixMsgStoreRecord[]> {
133
+ const results: IFixMsgStoreRecord[] = []
134
+ for (let seq = fromSeqNum; seq <= toSeqNum; seq++) {
135
+ const record = await this.get(seq)
136
+ if (record) {
137
+ results.push(record)
138
+ }
139
+ }
140
+ return results
141
+ }
142
+
143
+ // Lifecycle
144
+
145
+ async initialize (): Promise<void> {
146
+ await this.loadSeqNums()
147
+ await this.loadSessionTime()
148
+ await this.loadHeaderIndex()
149
+ this.streamProvider.openBody()
150
+ }
151
+
152
+ async flush (): Promise<void> {
153
+ await this.streamProvider.flush()
154
+ }
155
+
156
+ async dispose (): Promise<void> {
157
+ await this.streamProvider.flush()
158
+ if (this.ownsProvider) {
159
+ await this.streamProvider.dispose()
160
+ }
161
+ }
162
+
163
+ // Private - persistence
164
+
165
+ private async persistSeqNums (): Promise<void> {
166
+ const sender = this.senderSeqNumValue.toString().padStart(20, ' ')
167
+ const target = this.targetSeqNumValue.toString().padStart(20, ' ')
168
+ await this.streamProvider.writeSeqNums(`${sender} : ${target}`)
169
+ }
170
+
171
+ private async loadSeqNums (): Promise<void> {
172
+ const content = await this.streamProvider.readSeqNums()
173
+ if (content == null) {
174
+ this.senderSeqNumValue = 1
175
+ this.targetSeqNumValue = 1
176
+ return
177
+ }
178
+ const parts = content.split(':')
179
+ if (parts.length === 2) {
180
+ this.senderSeqNumValue = parseInt(parts[0].trim(), 10) || 1
181
+ this.targetSeqNumValue = parseInt(parts[1].trim(), 10) || 1
182
+ }
183
+ }
184
+
185
+ private async persistSessionTime (): Promise<void> {
186
+ await this.streamProvider.writeSessionTime(FileSessionStore.formatSessionTime(this.creationTimeValue))
187
+ }
188
+
189
+ private async loadSessionTime (): Promise<void> {
190
+ const content = await this.streamProvider.readSessionTime()
191
+ if (content == null) {
192
+ this.creationTimeValue = new Date()
193
+ await this.persistSessionTime()
194
+ return
195
+ }
196
+ const parsed = FileSessionStore.parseSessionTime(content.trim())
197
+ this.creationTimeValue = parsed ?? new Date()
198
+ }
199
+
200
+ private async loadHeaderIndex (): Promise<void> {
201
+ const lines = await this.streamProvider.readHeaderLines()
202
+ for (const line of lines) {
203
+ if (!line.trim()) continue
204
+ const parts = line.split(',')
205
+ if (parts.length === 3) {
206
+ const seqNum = parseInt(parts[0], 10)
207
+ const offset = parseInt(parts[1], 10)
208
+ const length = parseInt(parts[2], 10)
209
+ if (!isNaN(seqNum) && !isNaN(offset) && !isNaN(length)) {
210
+ this.headerIndex.set(seqNum, { offset, length })
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ // Private - message reading
217
+
218
+ private async readMessage (seqNum: number, offset: number, length: number): Promise<IFixMsgStoreRecord | null> {
219
+ const buffer = await this.streamProvider.readBody(offset, length)
220
+ if (buffer.length !== length) return null
221
+
222
+ const encoded = buffer.toString('utf8')
223
+ const msgType = FileSessionStore.extractTag(encoded, '35')
224
+ const sendingTimeStr = FileSessionStore.extractTag(encoded, '52')
225
+ let timestamp = new Date(0)
226
+ if (sendingTimeStr) {
227
+ const parsed = FileSessionStore.parseSessionTime(sendingTimeStr)
228
+ if (parsed) timestamp = parsed
229
+ }
230
+
231
+ return new FixMsgStoreRecord(msgType ?? '', timestamp, seqNum, undefined, encoded)
232
+ }
233
+
234
+ /**
235
+ * Extract a FIX tag value from a raw message string.
236
+ * Tags are delimited by SOH (0x01) or pipe (|).
237
+ */
238
+ static extractTag (message: string, tag: string): string | null {
239
+ const tagPrefix = `${tag}=`
240
+
241
+ // Check if tag is at the start
242
+ if (message.startsWith(tagPrefix)) {
243
+ const endIndex = FileSessionStore.findDelimiter(message, tagPrefix.length)
244
+ return message.substring(tagPrefix.length, endIndex)
245
+ }
246
+
247
+ // Search for SOH + tag= or | + tag=
248
+ for (const delim of ['\x01', '|']) {
249
+ const search = `${delim}${tagPrefix}`
250
+ const idx = message.indexOf(search)
251
+ if (idx >= 0) {
252
+ const startIndex = idx + search.length
253
+ const endIndex = FileSessionStore.findDelimiter(message, startIndex)
254
+ return message.substring(startIndex, endIndex)
255
+ }
256
+ }
257
+
258
+ return null
259
+ }
260
+
261
+ private static findDelimiter (message: string, fromIndex: number): number {
262
+ for (let i = fromIndex; i < message.length; i++) {
263
+ const ch = message.charAt(i)
264
+ if (ch === '\x01' || ch === '|') return i
265
+ }
266
+ return message.length
267
+ }
268
+
269
+ /**
270
+ * Format a date as QuickFix session time: YYYYMMDD-HH:MM:SS.ffffff
271
+ */
272
+ static formatSessionTime (date: Date): string {
273
+ const y = date.getUTCFullYear()
274
+ const M = (date.getUTCMonth() + 1).toString().padStart(2, '0')
275
+ const d = date.getUTCDate().toString().padStart(2, '0')
276
+ const h = date.getUTCHours().toString().padStart(2, '0')
277
+ const m = date.getUTCMinutes().toString().padStart(2, '0')
278
+ const s = date.getUTCSeconds().toString().padStart(2, '0')
279
+ const ms = date.getUTCMilliseconds().toString().padStart(3, '0')
280
+ return `${y}${M}${d}-${h}:${m}:${s}.${ms}000`
281
+ }
282
+
283
+ /**
284
+ * Parse a QuickFix session time string: YYYYMMDD-HH:MM:SS.fff[fff]
285
+ */
286
+ static parseSessionTime (str: string): Date | null {
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})$/)
289
+ if (!match) return null
290
+ const [, y, M, d, h, m, s, frac] = match
291
+ const ms = parseInt(frac.substring(0, 3), 10)
292
+ return new Date(Date.UTC(parseInt(y), parseInt(M) - 1, parseInt(d), parseInt(h), parseInt(m), parseInt(s), ms))
293
+ }
294
+ }
@@ -0,0 +1,123 @@
1
+ import * as fs from 'fs'
2
+ import { ISessionStreamProvider } from './session-stream-provider'
3
+ import { SessionId } from './session-id'
4
+
5
+ /**
6
+ * File-based implementation of ISessionStreamProvider.
7
+ * Creates QuickFix-compatible files in the specified directory.
8
+ *
9
+ * Files created:
10
+ * - {prefix}.body — concatenated raw FIX messages
11
+ * - {prefix}.header — index lines: "seqnum,offset,length"
12
+ * - {prefix}.seqnums — sender/target sequence numbers
13
+ * - {prefix}.session — session creation time
14
+ */
15
+ export class FileSessionStreamProvider implements ISessionStreamProvider {
16
+ private readonly bodyPath: string
17
+ private readonly headerPath: string
18
+ private readonly seqNumsPath: string
19
+ private readonly sessionPath: string
20
+ private bodyFd: number | null = null
21
+ private bodySize: number = 0
22
+
23
+ constructor (
24
+ sessionId: SessionId,
25
+ directory: string
26
+ ) {
27
+ fs.mkdirSync(directory, { recursive: true })
28
+ this.bodyPath = sessionId.getFilePath(directory, 'body')
29
+ this.headerPath = sessionId.getFilePath(directory, 'header')
30
+ this.seqNumsPath = sessionId.getFilePath(directory, 'seqnums')
31
+ this.sessionPath = sessionId.getFilePath(directory, 'session')
32
+ }
33
+
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
39
+ }
40
+
41
+ async appendBody (data: Buffer): Promise<number> {
42
+ if (this.bodyFd === null) {
43
+ this.openBody()
44
+ }
45
+ const offset = this.bodySize
46
+ fs.writeSync(this.bodyFd!, data, 0, data.length, offset)
47
+ this.bodySize += data.length
48
+ return offset
49
+ }
50
+
51
+ async readBody (offset: number, length: number): Promise<Buffer> {
52
+ if (this.bodyFd === null) {
53
+ this.openBody()
54
+ }
55
+ const buf = Buffer.alloc(length)
56
+ fs.readSync(this.bodyFd!, buf, 0, length, offset)
57
+ return buf
58
+ }
59
+
60
+ getBodySize (): number {
61
+ return this.bodySize
62
+ }
63
+
64
+ async appendHeaderLine (line: string): Promise<void> {
65
+ await fs.promises.appendFile(this.headerPath, line + '\n', 'utf8')
66
+ }
67
+
68
+ async readHeaderLines (): Promise<string[]> {
69
+ if (!fs.existsSync(this.headerPath)) return []
70
+ const content = await fs.promises.readFile(this.headerPath, 'utf8')
71
+ if (!content.trim()) return []
72
+ return content.split('\n').filter(l => l.trim().length > 0)
73
+ }
74
+
75
+ async readSeqNums (): Promise<string | null> {
76
+ if (!fs.existsSync(this.seqNumsPath)) return null
77
+ return await fs.promises.readFile(this.seqNumsPath, 'utf8')
78
+ }
79
+
80
+ async writeSeqNums (content: string): Promise<void> {
81
+ await fs.promises.writeFile(this.seqNumsPath, content, 'utf8')
82
+ }
83
+
84
+ async readSessionTime (): Promise<string | null> {
85
+ if (!fs.existsSync(this.sessionPath)) return null
86
+ return await fs.promises.readFile(this.sessionPath, 'utf8')
87
+ }
88
+
89
+ async writeSessionTime (content: string): Promise<void> {
90
+ await fs.promises.writeFile(this.sessionPath, content, 'utf8')
91
+ }
92
+
93
+ async reset (): Promise<void> {
94
+ if (this.bodyFd !== null) {
95
+ fs.closeSync(this.bodyFd)
96
+ this.bodyFd = null
97
+ }
98
+ this.bodySize = 0
99
+ this.deleteIfExists(this.bodyPath)
100
+ this.deleteIfExists(this.headerPath)
101
+ this.deleteIfExists(this.seqNumsPath)
102
+ this.deleteIfExists(this.sessionPath)
103
+ }
104
+
105
+ async flush (): Promise<void> {
106
+ if (this.bodyFd !== null) {
107
+ fs.fsyncSync(this.bodyFd)
108
+ }
109
+ }
110
+
111
+ async dispose (): Promise<void> {
112
+ if (this.bodyFd !== null) {
113
+ fs.closeSync(this.bodyFd)
114
+ this.bodyFd = null
115
+ }
116
+ }
117
+
118
+ private deleteIfExists (filePath: string): void {
119
+ if (fs.existsSync(filePath)) {
120
+ fs.unlinkSync(filePath)
121
+ }
122
+ }
123
+ }
@@ -87,7 +87,7 @@ export class FixMsgAsciiStoreResend {
87
87
  private sequenceResetGap (startGap: number, newSeq: number): IFixMsgStoreRecord {
88
88
  const factory = this.config.factory
89
89
  const gapFill: ISequenceReset = factory?.sequenceReset(newSeq, true) as ISequenceReset
90
- gapFill.StandardHeader = factory?.header(MsgType.SequenceReset, startGap) as IStandardHeader
90
+ gapFill.StandardHeader = factory?.header(MsgType.SequenceReset, startGap, new Date()) as IStandardHeader
91
91
  gapFill.StandardHeader.PossDupFlag = true
92
92
 
93
93
  return new FixMsgStoreRecord(
@@ -0,0 +1,31 @@
1
+ import { IFixSessionStore } from './fix-session-store'
2
+ import { SessionId } from './session-id'
3
+ import { MemorySessionStore } from './memory-session-store'
4
+ import { FileSessionStore } from './file-session-store'
5
+
6
+ /**
7
+ * Factory for creating session stores.
8
+ */
9
+ export interface IFixSessionStoreFactory {
10
+ create (sessionId: SessionId): IFixSessionStore
11
+ }
12
+
13
+ /**
14
+ * Factory for creating in-memory session stores.
15
+ */
16
+ export class MemorySessionStoreFactory implements IFixSessionStoreFactory {
17
+ create (sessionId: SessionId): IFixSessionStore {
18
+ return new MemorySessionStore(sessionId)
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Factory for creating file-based session stores.
24
+ */
25
+ export class FileSessionStoreFactory implements IFixSessionStoreFactory {
26
+ constructor (private readonly directory: string) {}
27
+
28
+ create (sessionId: SessionId): IFixSessionStore {
29
+ return FileSessionStore.createWithFiles(sessionId, this.directory)
30
+ }
31
+ }
@@ -0,0 +1,37 @@
1
+ import { SessionId } from './session-id'
2
+ import { IFixMsgStoreRecord } from './fix-msg-store-record'
3
+
4
+ /**
5
+ * Unified session store interface for FIX message persistence and sequence number management.
6
+ * Coordinates all persistence for a single FIX session:
7
+ * - Message storage (.body + .header files)
8
+ * - Sequence numbers (.seqnums file)
9
+ * - Session metadata (.session file)
10
+ *
11
+ * QuickFix-compatible file format for interoperability.
12
+ */
13
+ export interface IFixSessionStore {
14
+ readonly sessionId: SessionId
15
+
16
+ // Message Operations
17
+ put (record: IFixMsgStoreRecord): Promise<void>
18
+ get (seqNum: number): Promise<IFixMsgStoreRecord | null>
19
+ getRange (fromSeqNum: number, toSeqNum: number): Promise<IFixMsgStoreRecord[]>
20
+
21
+ // Sequence Number Operations
22
+ senderSeqNum: number
23
+ targetSeqNum: number
24
+ setSenderSeqNum (value: number): Promise<void>
25
+ setTargetSeqNum (value: number): Promise<void>
26
+ nextSenderSeqNum (): Promise<number>
27
+ nextTargetSeqNum (): Promise<number>
28
+
29
+ // Session Operations
30
+ readonly creationTime: Date
31
+ reset (): Promise<void>
32
+
33
+ // Lifecycle
34
+ initialize (): Promise<void>
35
+ flush (): Promise<void>
36
+ dispose (): Promise<void>
37
+ }
@@ -2,3 +2,12 @@ export * from './fix-msg-memory-store'
2
2
  export * from './fix-msg-store'
3
3
  export * from './fix-msg-store-record'
4
4
  export * from './fix-msg-ascii-store-resend'
5
+ export * from './session-id'
6
+ export * from './fix-session-store'
7
+ export * from './memory-session-store'
8
+ export * from './fix-session-store-factory'
9
+ export * from './session-stream-provider'
10
+ export * from './memory-session-stream-provider'
11
+ export * from './file-session-stream-provider'
12
+ export * from './file-session-store'
13
+ export * from './store-config'