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.
- package/BACKPORT_PLAN.md +138 -79
- package/dist/buffer/fixml/fixml-view.js.map +1 -1
- package/dist/buffer/msg-encoder.js +34 -1
- package/dist/buffer/msg-encoder.js.map +1 -1
- package/dist/buffer/msg-parser.js +34 -1
- package/dist/buffer/msg-parser.js.map +1 -1
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/collections/index.js +1 -0
- package/dist/config/js-fix-config.d.ts +2 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/config/winston-logger.js.map +1 -1
- package/dist/dict-parser.js +34 -1
- package/dist/dict-parser.js.map +1 -1
- package/dist/dictionary/compiler/enum-compiler.js +37 -4
- package/dist/dictionary/compiler/enum-compiler.js.map +1 -1
- package/dist/dictionary/compiler/msg-compiler.js +36 -3
- package/dist/dictionary/compiler/msg-compiler.js.map +1 -1
- package/dist/dictionary/compiler/standard-snippet.js +34 -1
- package/dist/dictionary/compiler/standard-snippet.js.map +1 -1
- package/dist/dictionary/contained/contained-field-set.js +2 -0
- package/dist/dictionary/contained/contained-field-set.js.map +1 -1
- package/dist/dictionary/definition/simple-field-definition.js +34 -1
- package/dist/dictionary/definition/simple-field-definition.js.map +1 -1
- package/dist/dictionary/fix-parser.js +34 -1
- package/dist/dictionary/fix-parser.js.map +1 -1
- package/dist/dictionary/parser/fix-repository/repository-type.js +1 -0
- package/dist/dictionary/parser/fix-repository/repository-xml-parser.js +35 -2
- package/dist/dictionary/parser/fix-repository/repository-xml-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/fields-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/fix-xsd-parser.js +34 -1
- package/dist/dictionary/parser/fixml/fix-xsd-parser.js.map +1 -1
- package/dist/dictionary/parser/fixml/include-graph.js +35 -2
- package/dist/dictionary/parser/fixml/include-graph.js.map +1 -1
- package/dist/dictionary/parser/fixml/node-definitions.js +1 -0
- package/dist/dictionary/parser/fixml/xsd-parser.js +34 -1
- package/dist/dictionary/parser/fixml/xsd-parser.js.map +1 -1
- package/dist/jsfix-cmd.js +39 -3
- package/dist/jsfix-cmd.js.map +1 -1
- package/dist/runtime/session-launcher.js +34 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/sample/http/oms/app.js +34 -1
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/recovering-skeleton/app.js +34 -1
- package/dist/sample/tcp/recovering-skeleton/app.js.map +1 -1
- package/dist/store/file-session-store.d.ts +42 -0
- package/dist/store/file-session-store.js +256 -0
- package/dist/store/file-session-store.js.map +1 -0
- package/dist/store/file-session-stream-provider.d.ts +25 -0
- package/dist/store/file-session-stream-provider.js +162 -0
- package/dist/store/file-session-stream-provider.js.map +1 -0
- package/dist/store/fix-msg-ascii-store-resend.js +1 -1
- package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +13 -0
- package/dist/store/fix-session-store-factory.js +21 -0
- package/dist/store/fix-session-store-factory.js.map +1 -0
- package/dist/store/fix-session-store.d.ts +19 -0
- package/dist/store/fix-session-store.js +3 -0
- package/dist/store/fix-session-store.js.map +1 -0
- package/dist/store/index.d.ts +9 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/memory-session-store.d.ts +27 -0
- package/dist/store/memory-session-store.js +104 -0
- package/dist/store/memory-session-store.js.map +1 -0
- package/dist/store/memory-session-stream-provider.d.ts +26 -0
- package/dist/store/memory-session-stream-provider.js +103 -0
- package/dist/store/memory-session-stream-provider.js.map +1 -0
- package/dist/store/session-id.d.ts +9 -0
- package/dist/store/session-id.js +55 -0
- package/dist/store/session-id.js.map +1 -0
- package/dist/store/session-stream-provider.d.ts +15 -0
- package/dist/store/session-stream-provider.js +3 -0
- package/dist/store/session-stream-provider.js.map +1 -0
- package/dist/store/store-config.d.ts +4 -0
- package/dist/store/store-config.js +3 -0
- package/dist/store/store-config.js.map +1 -0
- package/dist/transport/ascii/ascii-session.d.ts +12 -1
- package/dist/transport/ascii/ascii-session.js +154 -5
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/http-duplex.js +4 -1
- package/dist/transport/duplex/http-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.js +34 -1
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/fix-acceptor.js +34 -1
- package/dist/transport/fix-acceptor.js.map +1 -1
- package/dist/transport/fix-entity.js +34 -1
- package/dist/transport/fix-entity.js.map +1 -1
- package/dist/transport/fixml/fixml-msg-transmitter.js +1 -1
- package/dist/transport/fixml/fixml-msg-transmitter.js.map +1 -1
- package/dist/transport/http/http-acceptor.js +34 -1
- package/dist/transport/http/http-acceptor.js.map +1 -1
- package/dist/transport/msg-transmitter.js +34 -1
- package/dist/transport/msg-transmitter.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -1
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/fix-clock.d.ts +6 -0
- package/dist/transport/session/fix-clock.js +10 -0
- package/dist/transport/session/fix-clock.js.map +1 -0
- package/dist/transport/session/fix-session.d.ts +1 -0
- package/dist/transport/session/fix-session.js +37 -1
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +4 -0
- package/dist/transport/session/index.js +4 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +69 -0
- package/dist/transport/session/resend-request-manager.js +208 -0
- package/dist/transport/session/resend-request-manager.js.map +1 -0
- package/dist/transport/session/session-description.d.ts +2 -0
- package/dist/transport/session/session-description.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -1
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-sequence-coordinator.d.ts +38 -0
- package/dist/transport/session/session-sequence-coordinator.js +180 -0
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -0
- package/dist/transport/session/session-sequence-store.d.ts +14 -0
- package/dist/transport/session/session-sequence-store.js +36 -0
- package/dist/transport/session/session-sequence-store.js.map +1 -0
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +34 -1
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/types/FIX4.4/index.js +1 -0
- package/dist/util/buffer-helper.js +34 -1
- package/dist/util/buffer-helper.js.map +1 -1
- package/dist/util/definition-factory.js +35 -2
- package/dist/util/definition-factory.js.map +1 -1
- package/jsfix.test_client.txt +67 -66
- package/jsfix.test_server.txt +64 -63
- package/package.json +11 -10
- package/src/buffer/fixml/fixml-view.ts +1 -1
- package/src/buffer/msg-view.ts +1 -1
- package/src/config/js-fix-config.ts +2 -0
- package/src/config/winston-logger.ts +3 -3
- package/src/dictionary/contained/contained-field-set.ts +2 -1
- package/src/dictionary/parser/fixml/fields-parser.ts +2 -2
- package/src/jsfix-cmd.ts +1 -1
- package/src/store/file-session-store.ts +294 -0
- package/src/store/file-session-stream-provider.ts +123 -0
- package/src/store/fix-msg-ascii-store-resend.ts +1 -1
- package/src/store/fix-session-store-factory.ts +31 -0
- package/src/store/fix-session-store.ts +37 -0
- package/src/store/index.ts +9 -0
- package/src/store/memory-session-store.ts +102 -0
- package/src/store/memory-session-stream-provider.ts +97 -0
- package/src/store/session-id.ts +32 -0
- package/src/store/session-stream-provider.ts +74 -0
- package/src/store/store-config.ts +15 -0
- package/src/transport/ascii/ascii-session.ts +218 -6
- package/src/transport/fixml/fixml-msg-transmitter.ts +1 -1
- package/src/transport/http/http-acceptor.ts +1 -1
- package/src/transport/session/a-session-msg-factory.ts +1 -1
- package/src/transport/session/fix-clock.ts +9 -0
- package/src/transport/session/fix-session.ts +5 -0
- package/src/transport/session/index.ts +4 -0
- package/src/transport/session/resend-request-manager.ts +268 -0
- package/src/transport/session/session-description.ts +2 -0
- package/src/transport/session/session-msg-factory.ts +1 -1
- package/src/transport/session/session-sequence-coordinator.ts +272 -0
- package/src/transport/session/session-sequence-store.ts +33 -0
- 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.
|
|
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.
|
|
79
|
+
"axios": "^1.14.0",
|
|
79
80
|
"express": "^5.2.1",
|
|
80
81
|
"lodash": "^4.17.23",
|
|
81
|
-
"mathjs": "^15.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
|
6
|
+
import moment = require('moment')
|
|
7
7
|
import { TagType } from '../tag/tag-type'
|
|
8
8
|
|
|
9
9
|
export class FixmlView extends MsgView {
|
package/src/buffer/msg-view.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -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'
|