jspurefix 5.8.0 → 5.8.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspurefix",
3
- "version": "5.8.0",
3
+ "version": "5.8.5",
4
4
  "description": "pure node js fix engine",
5
5
  "keywords": [
6
6
  "typescript",
@@ -44,8 +44,8 @@
44
44
  "repo44-bench-tc": "node dist/jsfix-cmd jsfix-cmd --dict=repo44 --fix=data/examples/FIX.4.4/repo/trade-capture/fix.txt --benchmark --delimiter=\"|\" --repeats=30000",
45
45
  "repo44-bench-sd": "node dist/jsfix-cmd --dict=repo44 --fix=data/examples/FIX.4.4/repo/security-definition/fix.txt --benchmark --delimiter=\"|\" --repeats=150000",
46
46
  "repo44-bench-lo": "node dist/jsfix-cmd --dict=repo44 --fix=data/examples/FIX.4.4/repo/logon/fix.txt --benchmark --delimiter=\"|\" --repeats=250000",
47
- "qf-bench-lo": "node dist/jsfix-cmd --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/quickfix/logon/fix.txt --benchmark --delimiter=\"|\" --repeats=250000",
48
- "qf-bench-hb": "node dist/jsfix-cmd --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/quickfix/heartbeat/fix.txt --benchmark --delimiter=\"|\" --repeats=250000",
47
+ "qf-bench-lo": "node dist/jsfix-cmd --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/quickfix/logon/fix.txt --benchmark --delimiter=\"|\" --repeats=1000000",
48
+ "qf-bench-hb": "node dist/jsfix-cmd --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/quickfix/heartbeat/fix.txt --benchmark --delimiter=\"|\" --repeats=1000000",
49
49
  "fixml": "node dist/jsfix-cmd --dict=repofixml",
50
50
  "tcp-qf-md": "node dist/sample/tcp/qf-md/app",
51
51
  "tcp-tls-tc": "node dist/sample/tcp/tls-trade-capture/app",
@@ -74,9 +74,9 @@
74
74
  "author": "",
75
75
  "license": "MIT",
76
76
  "dependencies": {
77
- "@jest/globals": "^30.3.0",
77
+ "@jest/globals": "^30.4.1",
78
78
  "align-text": "^1.0.2",
79
- "axios": "^1.14.0",
79
+ "axios": "^1.16.1",
80
80
  "express": "^5.2.1",
81
81
  "lodash": "^4.18.1",
82
82
  "mathjs": "^15.2.0",
@@ -99,16 +99,16 @@
99
99
  "@types/lodash": "^4.17.24",
100
100
  "@types/mathjs": "^9.4.2",
101
101
  "@types/minimist": "^1.2.5",
102
- "@types/node": "^25.5.2",
102
+ "@types/node": "^25.9.1",
103
103
  "@types/request-promise-native": "^1.0.21",
104
104
  "@types/sax": "^1.2.7",
105
105
  "@types/winston": "^2.4.4",
106
106
  "eslint": "^9.39.4",
107
107
  "eslint-config-love": "^151.0.0",
108
- "jest": "^30.3.0",
108
+ "jest": "^30.4.2",
109
109
  "madge": "^8.0.0",
110
110
  "standard": "^17.1.2",
111
- "ts-jest": "^29.4.9",
112
- "typescript": "^6.0.2"
111
+ "ts-jest": "^29.4.11",
112
+ "typescript": "^6.0.3"
113
113
  }
114
114
  }
@@ -30,7 +30,8 @@ export class AsciiParser extends MsgParser {
30
30
 
31
31
  constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig,
32
32
  @inject(DITokens.readStream) public readonly readStream: Readable | null,
33
- @inject(DITokens.ParseBuffer) protected readonly receivingBuffer: ElasticBuffer) {
33
+ @inject(DITokens.ParseBuffer) protected readonly receivingBuffer: ElasticBuffer,
34
+ @inject(DITokens.maxMessageLen) public readonly maxMessageLen: number = 160 * 1024) {
34
35
  super()
35
36
 
36
37
  this.delimiter = config.delimiter ?? AsciiChars.Soh
@@ -38,7 +39,7 @@ export class AsciiParser extends MsgParser {
38
39
  this.id = AsciiParser.nextId++
39
40
  this.segmentParser = config.sessionContainer.resolve<AsciiSegmentParser>(AsciiSegmentParser)
40
41
  this.state = config.sessionContainer.resolve<AsciiParserState>(AsciiParserState)
41
- this.state.locations = new Tags(this.receivingBuffer.size / 10)
42
+ this.state.locations = new Tags(this.receivingBuffer.size / 10, this.maxMessageLen)
42
43
  this.state.definitions = this.config.definitions
43
44
  this.state.beginMessage()
44
45
  if (readStream !== null) {
@@ -8,10 +8,11 @@ export class Tags {
8
8
  public static readonly CheckSumTag: number = MsgTag.CheckSum
9
9
  public static readonly MsgTag: number = MsgTag.MsgType
10
10
 
11
- public tagPos: TagPos[] = new Array(this.startingLength)
11
+ public tagPos: TagPos[] = new Array(Math.min(this.startingLength, this.maxLength))
12
12
  public nextTagPos: number = 0
13
13
 
14
- constructor (public readonly startingLength: number = 30 * 1000) {
14
+ constructor (public readonly startingLength: number = 30 * 1000,
15
+ public readonly maxLength: number = 1024 * 1024) {
15
16
  }
16
17
 
17
18
  public static toJSType (tagType: TagType): string {
@@ -101,7 +102,7 @@ export class Tags {
101
102
 
102
103
  public clone (): Tags {
103
104
  const next: number = this.nextTagPos
104
- const cloned: Tags = new Tags(next)
105
+ const cloned: Tags = new Tags(next, this.maxLength)
105
106
  cloned.nextTagPos = next
106
107
  for (let i = 0; i < next; ++i) {
107
108
  cloned.tagPos[i] = this.tagPos[i].clone()
@@ -128,7 +129,10 @@ export class Tags {
128
129
  }
129
130
 
130
131
  private expand (): void {
131
- const size = this.tagPos.length * 2
132
+ if (this.tagPos.length >= this.maxLength) {
133
+ throw new Error(`tag count exceeds maximum of ${this.maxLength} - message too large or stream misaligned`)
134
+ }
135
+ const size = Math.min(this.tagPos.length * 2, this.maxLength)
132
136
  const tagPos = new Array(size)
133
137
  for (let i = 0; i < this.tagPos.length; ++i) {
134
138
  tagPos[i] = this.tagPos[i]
@@ -101,7 +101,9 @@ export abstract class AsciiSession extends FixSession {
101
101
  this.sessionLogger.info('logon with ResetSeqNumFlag=Y, accepting regardless of sequence')
102
102
  const seqNo = view.getTyped(MsgTag.MsgSeqNum) as number
103
103
  this.sessionState.lastPeerMsgSeqNum = seqNo
104
- this.coordinator.onMessageReceived(seqNo, false)
104
+ this.coordinator.onMessageReceived(seqNo, false).catch((e: Error) => {
105
+ this.sessionLogger.warning(`coordinator.onMessageReceived store persist failed: ${e.message}`)
106
+ })
105
107
  return true
106
108
  }
107
109
  }
@@ -119,7 +121,9 @@ export abstract class AsciiSession extends FixSession {
119
121
  const possDupFlag = view.getTyped(MsgTag.PossDupFlag) as boolean | undefined
120
122
  if (possDupFlag === true) {
121
123
  this.sessionLogger.debug(`message '${msgType}' has PossDupFlag=Y, bypassing sequence check`)
122
- this.coordinator.onMessageReceived(seqNo, true)
124
+ this.coordinator.onMessageReceived(seqNo, true).catch((e: Error) => {
125
+ this.sessionLogger.warning(`coordinator.onMessageReceived store persist failed: ${e.message}`)
126
+ })
123
127
  return true
124
128
  }
125
129
  // Check if this is a delayed message that fills a pending gap range.
@@ -127,7 +131,9 @@ export abstract class AsciiSession extends FixSession {
127
131
  const inPendingGapRange = pendingRequests.some(p => seqNo >= p.begin && seqNo <= p.end)
128
132
  if (inPendingGapRange) {
129
133
  this.sessionLogger.info(`accepting delayed message seq ${seqNo} (in pending gap range)`)
130
- this.coordinator.onMessageReceived(seqNo, false)
134
+ this.coordinator.onMessageReceived(seqNo, false).catch((e: Error) => {
135
+ this.sessionLogger.warning(`coordinator.onMessageReceived store persist failed: ${e.message}`)
136
+ })
131
137
  return true
132
138
  }
133
139
  // serious problem ... drop immediately
@@ -171,7 +177,9 @@ export abstract class AsciiSession extends FixSession {
171
177
 
172
178
  // Update sequence state regardless — the gap-triggering message is valid.
173
179
  state.lastPeerMsgSeqNum = seqNo
174
- this.coordinator.onMessageReceived(seqNo, false)
180
+ this.coordinator.onMessageReceived(seqNo, false).catch((e: Error) => {
181
+ this.sessionLogger.warning(`coordinator.onMessageReceived store persist failed: ${e.message}`)
182
+ })
175
183
 
176
184
  // Logon and ResendRequest were already fully handled above (peerLogon / onResendRequest).
177
185
  // Don't return true for them — they must not be dispatched to onSessionMsg again.
@@ -182,7 +190,9 @@ export abstract class AsciiSession extends FixSession {
182
190
  } else {
183
191
  ret = true
184
192
  state.lastPeerMsgSeqNum = seqNo
185
- this.coordinator.onMessageReceived(seqNo, false)
193
+ this.coordinator.onMessageReceived(seqNo, false).catch((e: Error) => {
194
+ this.sessionLogger.warning(`coordinator.onMessageReceived store persist failed: ${e.message}`)
195
+ })
186
196
  }
187
197
 
188
198
  // Reset timeout recovery on successful message receipt
@@ -333,7 +343,9 @@ export abstract class AsciiSession extends FixSession {
333
343
  this.sessionState.lastPeerMsgSeqNum = this.coordinator.lastProcessedPeerSeqNum
334
344
 
335
345
  if (this.store) {
336
- this.store.clear()
346
+ this.store.clear().catch((e: Error) => {
347
+ this.sessionLogger.warning(`store.clear failed: ${e.message}`)
348
+ })
337
349
  this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
338
350
  }
339
351
 
@@ -439,7 +451,9 @@ export abstract class AsciiSession extends FixSession {
439
451
  // expect newSeqNo to be the next message's sequence number.
440
452
  this.sessionState.lastPeerMsgSeqNum = newSeqNo - 1
441
453
  // Notify coordinator to update expected target and clear pending resend requests
442
- this.coordinator.onGapFillReceived(gapFillSeq, newSeqNo)
454
+ this.coordinator.onGapFillReceived(gapFillSeq, newSeqNo).catch((e: Error) => {
455
+ this.sessionLogger.warning(`coordinator.onGapFillReceived store persist failed: ${e.message}`)
456
+ })
443
457
  break
444
458
  }
445
459
 
@@ -524,7 +538,9 @@ export abstract class AsciiSession extends FixSession {
524
538
 
525
539
  // Fire-and-forget the async coordinator call (store updates resolve on next microtask)
526
540
  // but compute the expected values synchronously since we know the reset outcome
527
- this.coordinator.handlePeerReset(peerSeqNum, weAlsoReset)
541
+ this.coordinator.handlePeerReset(peerSeqNum, weAlsoReset).catch((e: Error) => {
542
+ this.sessionLogger.warning(`coordinator.handlePeerReset store persist failed: ${e.message}`)
543
+ })
528
544
  if (transmitter) {
529
545
  transmitter.msgSeqNum = savedEncoderSeqNum ?? 1
530
546
  }
@@ -532,7 +548,9 @@ export abstract class AsciiSession extends FixSession {
532
548
 
533
549
  // Recreate resender with empty store
534
550
  if (this.store) {
535
- this.store.clear()
551
+ this.store.clear().catch((e: Error) => {
552
+ this.sessionLogger.warning(`store.clear failed: ${e.message}`)
553
+ })
536
554
  this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
537
555
  }
538
556
  logger.info(`reset complete: encoderSeqNum=${transmitter?.msgSeqNum}, lastPeerMsgSeqNum=${peerSeqNum}`)
@@ -555,14 +573,18 @@ export abstract class AsciiSession extends FixSession {
555
573
  if (weReset && resetSeqNumFlag !== true) {
556
574
  logger.info('acceptor sending ResetSeqNumFlag=Y (peer sent N), resetting sequences')
557
575
  // Fire-and-forget async coordinator call, set values synchronously
558
- this.coordinator.resetAsAcceptor()
576
+ this.coordinator.resetAsAcceptor().catch((e: Error) => {
577
+ this.sessionLogger.warning(`coordinator.resetAsAcceptor store persist failed: ${e.message}`)
578
+ })
559
579
  const transmitter = this.transport?.transmitter as AsciiMsgTransmitter | undefined
560
580
  if (transmitter) {
561
581
  transmitter.msgSeqNum = 1
562
582
  }
563
583
  this.sessionState.lastPeerMsgSeqNum = 0
564
584
  if (this.store) {
565
- this.store.clear()
585
+ this.store.clear().catch((e: Error) => {
586
+ this.sessionLogger.warning(`store.clear failed: ${e.message}`)
587
+ })
566
588
  this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
567
589
  }
568
590
  }
@@ -110,14 +110,26 @@ export abstract class FixSession extends events.EventEmitter {
110
110
  this.setState(SessionState.WaitingForALogon)
111
111
  }
112
112
 
113
- this.on('error', (e: Error) => {
113
+ // Use named handlers and detach BOTH when the promise settles. Only one of
114
+ // 'error'/'done' fires per run; without removing the sibling the unfired
115
+ // listener would leak on every reconnect (issue #146). `once` alone is not
116
+ // enough for the same reason — the sibling never fires, so never self-detaches.
117
+ const cleanup = (): void => {
118
+ this.removeListener('error', onError)
119
+ this.removeListener('done', onDone)
120
+ }
121
+ const onError = (e: Error): void => {
122
+ cleanup()
114
123
  logger.error(e)
115
124
  reject(e)
116
- })
117
-
118
- this.on('done', () => {
125
+ }
126
+ const onDone = (): void => {
127
+ cleanup()
119
128
  resolve(this.transport?.id)
120
- })
129
+ }
130
+
131
+ this.on('error', onError)
132
+ this.on('done', onDone)
121
133
  })
122
134
  }
123
135