jspurefix 5.2.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 (54) hide show
  1. package/BACKPORT_PLAN.md +15 -29
  2. package/dist/config/js-fix-config.d.ts +2 -0
  3. package/dist/config/js-fix-config.js.map +1 -1
  4. package/dist/store/file-session-store.d.ts +42 -0
  5. package/dist/store/file-session-store.js +256 -0
  6. package/dist/store/file-session-store.js.map +1 -0
  7. package/dist/store/file-session-stream-provider.d.ts +25 -0
  8. package/dist/store/file-session-stream-provider.js +162 -0
  9. package/dist/store/file-session-stream-provider.js.map +1 -0
  10. package/dist/store/fix-session-store-factory.d.ts +13 -0
  11. package/dist/store/fix-session-store-factory.js +21 -0
  12. package/dist/store/fix-session-store-factory.js.map +1 -0
  13. package/dist/store/fix-session-store.d.ts +19 -0
  14. package/dist/store/fix-session-store.js +3 -0
  15. package/dist/store/fix-session-store.js.map +1 -0
  16. package/dist/store/index.d.ts +9 -0
  17. package/dist/store/index.js +9 -0
  18. package/dist/store/index.js.map +1 -1
  19. package/dist/store/memory-session-store.d.ts +27 -0
  20. package/dist/store/memory-session-store.js +104 -0
  21. package/dist/store/memory-session-store.js.map +1 -0
  22. package/dist/store/memory-session-stream-provider.d.ts +26 -0
  23. package/dist/store/memory-session-stream-provider.js +103 -0
  24. package/dist/store/memory-session-stream-provider.js.map +1 -0
  25. package/dist/store/session-id.d.ts +9 -0
  26. package/dist/store/session-id.js +55 -0
  27. package/dist/store/session-id.js.map +1 -0
  28. package/dist/store/session-stream-provider.d.ts +15 -0
  29. package/dist/store/session-stream-provider.js +3 -0
  30. package/dist/store/session-stream-provider.js.map +1 -0
  31. package/dist/store/store-config.d.ts +4 -0
  32. package/dist/store/store-config.js +3 -0
  33. package/dist/store/store-config.js.map +1 -0
  34. package/dist/transport/ascii/ascii-session.d.ts +6 -1
  35. package/dist/transport/ascii/ascii-session.js +37 -5
  36. package/dist/transport/ascii/ascii-session.js.map +1 -1
  37. package/dist/transport/session/session-description.d.ts +2 -0
  38. package/dist/transport/session/session-description.js.map +1 -1
  39. package/jsfix.test_client.txt +67 -67
  40. package/jsfix.test_server.txt +64 -64
  41. package/package.json +1 -1
  42. package/src/config/js-fix-config.ts +2 -0
  43. package/src/store/file-session-store.ts +294 -0
  44. package/src/store/file-session-stream-provider.ts +123 -0
  45. package/src/store/fix-session-store-factory.ts +31 -0
  46. package/src/store/fix-session-store.ts +37 -0
  47. package/src/store/index.ts +9 -0
  48. package/src/store/memory-session-store.ts +102 -0
  49. package/src/store/memory-session-stream-provider.ts +97 -0
  50. package/src/store/session-id.ts +32 -0
  51. package/src/store/session-stream-provider.ts +74 -0
  52. package/src/store/store-config.ts +15 -0
  53. package/src/transport/ascii/ascii-session.ts +57 -6
  54. package/src/transport/session/session-description.ts +2 -0
@@ -0,0 +1,32 @@
1
+ import * as path from 'path'
2
+
3
+ /**
4
+ * Identifies a FIX session for file naming and lookup.
5
+ * Format: {BeginString}-{SenderCompID}-{TargetCompID}
6
+ */
7
+ export class SessionId {
8
+ constructor (
9
+ public readonly beginString: string,
10
+ public readonly senderCompID: string,
11
+ public readonly targetCompID: string
12
+ ) {}
13
+
14
+ /**
15
+ * Creates a file prefix for QuickFix-compatible file naming.
16
+ * Example: "FIX.4.4-SENDER-TARGET"
17
+ */
18
+ toFilePrefix (): string {
19
+ return `${this.beginString}-${this.senderCompID}-${this.targetCompID}`
20
+ }
21
+
22
+ /**
23
+ * Gets the full path for a specific file extension.
24
+ */
25
+ getFilePath (directory: string, extension: string): string {
26
+ return path.join(directory, `${this.toFilePrefix()}.${extension}`)
27
+ }
28
+
29
+ toString (): string {
30
+ return this.toFilePrefix()
31
+ }
32
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Provides stream access for session store operations.
3
+ * Allows abstraction of file I/O for testing with in-memory buffers.
4
+ */
5
+ export interface ISessionStreamProvider {
6
+ /**
7
+ * Opens or creates a read-write buffer for the message body file.
8
+ * Must support random-access reads (by offset+length).
9
+ */
10
+ openBody (): void
11
+
12
+ /**
13
+ * Appends data to the body. Returns the offset at which data was written.
14
+ */
15
+ appendBody (data: Buffer): Promise<number>
16
+
17
+ /**
18
+ * Reads data from the body at the given offset and length.
19
+ */
20
+ readBody (offset: number, length: number): Promise<Buffer>
21
+
22
+ /**
23
+ * Gets the current body size (for calculating offsets).
24
+ */
25
+ getBodySize (): number
26
+
27
+ /**
28
+ * Appends a line to the header index file.
29
+ */
30
+ appendHeaderLine (line: string): Promise<void>
31
+
32
+ /**
33
+ * Reads all lines from the header index file.
34
+ * Returns empty array if no data exists.
35
+ */
36
+ readHeaderLines (): Promise<string[]>
37
+
38
+ /**
39
+ * Reads the sequence numbers string.
40
+ * Returns null if no data exists.
41
+ */
42
+ readSeqNums (): Promise<string | null>
43
+
44
+ /**
45
+ * Writes the sequence numbers string.
46
+ */
47
+ writeSeqNums (content: string): Promise<void>
48
+
49
+ /**
50
+ * Reads the session time string.
51
+ * Returns null if no data exists.
52
+ */
53
+ readSessionTime (): Promise<string | null>
54
+
55
+ /**
56
+ * Writes the session time string.
57
+ */
58
+ writeSessionTime (content: string): Promise<void>
59
+
60
+ /**
61
+ * Resets all streams/files for a new session.
62
+ */
63
+ reset (): Promise<void>
64
+
65
+ /**
66
+ * Flushes any pending writes.
67
+ */
68
+ flush (): Promise<void>
69
+
70
+ /**
71
+ * Disposes of all resources.
72
+ */
73
+ dispose (): Promise<void>
74
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Configuration for session message store.
3
+ * Add to session description JSON to enable persistent storage.
4
+ *
5
+ * Examples:
6
+ * "store": { "type": "memory" } — explicit in-memory (default)
7
+ * "store": { "type": "file" } — file store in ./store directory
8
+ * "store": { "type": "file", "directory": "/var/fix/sessions" }
9
+ *
10
+ * Omitting the store block entirely uses in-memory storage.
11
+ */
12
+ export interface StoreConfig {
13
+ readonly type: 'memory' | 'file'
14
+ readonly directory?: string
15
+ }
@@ -2,22 +2,29 @@ import { MsgView } from '../../buffer'
2
2
  import { MsgTag, MsgType, SessionRejectReason } from '../../types'
3
3
  import { IJsFixConfig } from '../../config'
4
4
  import { FixSession } from '../session/fix-session'
5
- import { FixMsgAsciiStoreResend, FixMsgMemoryStore, IFixMsgStore, IFixMsgStoreRecord } from '../../store'
5
+ import {
6
+ FixMsgAsciiStoreResend, FixMsgMemoryStore, FixMsgStoreRecord,
7
+ IFixMsgStore, IFixMsgStoreRecord,
8
+ IFixSessionStore, IFixSessionStoreFactory,
9
+ MemorySessionStoreFactory, FileSessionStoreFactory, SessionId
10
+ } from '../../store'
6
11
  import { SessionState } from '../tcp'
7
12
  import { TickAction } from '../tick-action'
8
13
  import { IMsgApplication } from '../msg-application'
9
14
  import { SegmentType } from '../../buffer/segment/segment-type'
10
15
  import { SessionSequenceCoordinator } from '../session/session-sequence-coordinator'
11
- import { MemorySequenceStore } from '../session/session-sequence-store'
12
16
  import { DefaultFixClock } from '../session/fix-clock'
13
17
  import { ResendActionType } from '../session/resend-request-manager'
14
18
  import { AsciiMsgTransmitter } from './ascii-msg-transmitter'
19
+ import { ILooseObject } from '../../collections/collection'
15
20
 
16
21
  export abstract class AsciiSession extends FixSession {
17
22
  public heartbeat: boolean = true
18
23
  protected store: IFixMsgStore | null = null
19
24
  protected resender: FixMsgAsciiStoreResend
20
25
  protected readonly coordinator: SessionSequenceCoordinator
26
+ protected readonly sessionStore: IFixSessionStore
27
+ protected readonly sessionId: SessionId
21
28
 
22
29
  protected constructor (public readonly config: IJsFixConfig) {
23
30
  super(config)
@@ -26,9 +33,18 @@ export abstract class AsciiSession extends FixSession {
26
33
  this.store = new FixMsgMemoryStore(this.config.description.SenderCompId, this.config)
27
34
  this.resender = new FixMsgAsciiStoreResend(this.store, this.config)
28
35
 
29
- const sequenceStore = new MemorySequenceStore()
36
+ // Create session store from factory.
37
+ // Priority: programmatic config > JSON store config > default in-memory
38
+ const storeFactory = config.sessionStoreFactory ?? AsciiSession.createStoreFactory(config.description.store)
39
+ this.sessionId = new SessionId(
40
+ config.description.BeginString,
41
+ config.description.SenderCompId,
42
+ config.description.TargetCompID
43
+ )
44
+ this.sessionStore = storeFactory.create(this.sessionId)
45
+
30
46
  const clock = new DefaultFixClock()
31
- this.coordinator = new SessionSequenceCoordinator(sequenceStore, clock)
47
+ this.coordinator = new SessionSequenceCoordinator(this.sessionStore, clock)
32
48
  const lastReceivedSeqNum = config.description.LastReceivedSeqNum ?? 0
33
49
  this.coordinator.initializeFromConfig(undefined, lastReceivedSeqNum + 1)
34
50
  }
@@ -67,6 +83,14 @@ export abstract class AsciiSession extends FixSession {
67
83
  this.coordinator.onMessageReceived(seqNo, true)
68
84
  return true
69
85
  }
86
+ // Check if this is a delayed message that fills a pending gap range.
87
+ const pendingRequests = this.coordinator.pendingResendRequests
88
+ const inPendingGapRange = pendingRequests.some(p => seqNo >= p.begin && seqNo <= p.end)
89
+ if (inPendingGapRange) {
90
+ this.sessionLogger.info(`accepting delayed message seq ${seqNo} (in pending gap range)`)
91
+ this.coordinator.onMessageReceived(seqNo, false)
92
+ return true
93
+ }
70
94
  // serious problem ... drop immediately
71
95
  this.sessionLogger.warning(`terminate as seqDelta (${seqDelta}) < 0 lastSeq = ${lastSeq} seqNo = ${seqNo}`)
72
96
  this.stop()
@@ -106,8 +130,10 @@ export abstract class AsciiSession extends FixSession {
106
130
  }
107
131
  }
108
132
 
109
- // Gap message is not forwarded to application wait for resend to fill
110
- // (C# accepts and forwards, but that's a PR 3D behaviour change)
133
+ // Accept the current message don't block waiting for gap fill.
134
+ // The gap will be filled by the resend response, but this message is valid.
135
+ ret = true
136
+ state.lastPeerMsgSeqNum = seqNo
111
137
  this.coordinator.onMessageReceived(seqNo, false)
112
138
  } else {
113
139
  ret = true
@@ -250,9 +276,32 @@ export abstract class AsciiSession extends FixSession {
250
276
  this.sessionLogger.info('coordinator reset transient state for reconnect')
251
277
  }
252
278
 
279
+ protected override txOnEncoded (msgType: string, data: string, hdr: ILooseObject): void {
280
+ super.txOnEncoded(msgType, data, hdr)
281
+ // Store the encoded message in the session store for recovery/resend
282
+ const seqNum = hdr?.MsgSeqNum as number | undefined
283
+ if (seqNum != null) {
284
+ const record = new FixMsgStoreRecord(msgType, new Date(), seqNum, undefined, data)
285
+ this.sessionStore.put(record).catch((e: Error) => {
286
+ // Never block sends on store errors
287
+ this.sessionLogger.warning(`failed to store message seq=${seqNum}: ${e.message}`)
288
+ })
289
+ }
290
+ }
291
+
253
292
  private static readonly MaxLogonRetries = 100
254
293
  private static readonly MaxTimeoutRecoveryAttempts = 0
255
294
 
295
+ private static createStoreFactory (storeConfig?: { type: string, directory?: string }): IFixSessionStoreFactory {
296
+ if (!storeConfig) return new MemorySessionStoreFactory()
297
+ switch (storeConfig.type?.toLowerCase()) {
298
+ case 'file':
299
+ return new FileSessionStoreFactory(storeConfig.directory ?? 'store')
300
+ default:
301
+ return new MemorySessionStoreFactory()
302
+ }
303
+ }
304
+
256
305
  private handleLogonRejected (text: string | null): void {
257
306
  if (!this.coordinator.onLogonRejectedForSequence(AsciiSession.MaxLogonRetries)) {
258
307
  this.sessionLogger.warning(`max logon retries (${AsciiSession.MaxLogonRetries}) exceeded, giving up. Text='${text}'`)
@@ -488,6 +537,8 @@ export abstract class AsciiSession extends FixSession {
488
537
  const action: TickAction = sessionState.calcAction(new Date())
489
538
  const application: IMsgApplication | null = this.transport.config.description.application ?? null
490
539
  const logger = this.sessionLogger
540
+ // Clean up timed-out resend requests
541
+ this.coordinator.tick()
491
542
 
492
543
  switch (action) {
493
544
  case TickAction.Nothing: {
@@ -1,4 +1,5 @@
1
1
  import { IMsgApplication } from '../msg-application'
2
+ import { StoreConfig } from '../../store/store-config'
2
3
 
3
4
  export interface IDynamicSessionParams {
4
5
  readonly Name: string
@@ -18,4 +19,5 @@ export interface ISessionDescription extends IDynamicSessionParams {
18
19
  LastSentSeqNum?: number
19
20
  readonly LastReceivedSeqNum?: number
20
21
  readonly BodyLengthChars?: number
22
+ readonly store?: StoreConfig
21
23
  }