jspurefix 5.10.5 → 5.11.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/README.md +70 -0
- package/dist/runtime/session-launcher.d.ts +2 -0
- package/dist/runtime/session-launcher.js +13 -0
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/transport/ascii/ascii-session-msg-factory.d.ts +1 -1
- package/dist/transport/ascii/ascii-session-msg-factory.js +2 -2
- package/dist/transport/ascii/ascii-session-msg-factory.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +6 -0
- package/dist/transport/ascii/ascii-session.js +72 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/msg-application.d.ts +3 -0
- package/dist/transport/msg-application.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +1 -1
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/tcp/recovering-tcp-initiator.d.ts +6 -0
- package/dist/transport/tcp/recovering-tcp-initiator.js +55 -15
- package/dist/transport/tcp/recovering-tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator-connector.d.ts +2 -0
- package/dist/transport/tcp/tcp-initiator-connector.js +8 -3
- package/dist/transport/tcp/tcp-initiator-connector.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.d.ts +1 -0
- package/dist/transport/tcp/tcp-initiator.js +7 -40
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +1 -1
- package/src/runtime/session-launcher.ts +25 -3
- package/src/transport/ascii/ascii-session-msg-factory.ts +8 -2
- package/src/transport/ascii/ascii-session.ts +115 -7
- package/src/transport/msg-application.ts +16 -0
- package/src/transport/session/fix-session.ts +12 -2
- package/src/transport/tcp/recovering-tcp-initiator.ts +74 -13
- package/src/transport/tcp/tcp-initiator-connector.ts +6 -1
- package/src/transport/tcp/tcp-initiator.ts +12 -6
|
@@ -4,8 +4,24 @@ import { IHttpTransportDescription } from './http/http-transport-description'
|
|
|
4
4
|
export interface IMsgApplication {
|
|
5
5
|
readonly name: string
|
|
6
6
|
readonly type: string
|
|
7
|
+
/**
|
|
8
|
+
* An initiator which re-establishes its transport after the connection drops and
|
|
9
|
+
* resumes the same session, rather than ending when the socket does. Defaults to
|
|
10
|
+
* false, which gives a single connection. See "Reconnecting initiators" in the
|
|
11
|
+
* README for the retry policy below.
|
|
12
|
+
*/
|
|
7
13
|
readonly resilient: boolean
|
|
14
|
+
/** how long to wait between attempts whilst trying to establish a connection. Defaults to 5. */
|
|
8
15
|
readonly reconnectSeconds: number
|
|
16
|
+
/**
|
|
17
|
+
* how long to keep attempting a connection before giving up. Defaults to 60 for a
|
|
18
|
+
* resilient initiator and 22 for a plain one.
|
|
19
|
+
*/
|
|
20
|
+
readonly connectTimeoutSeconds?: number
|
|
21
|
+
/** resilient only: how long after losing the transport before trying to get it back. Defaults to 5. */
|
|
22
|
+
readonly recoveryAttemptSeconds?: number
|
|
23
|
+
/** resilient only: how long to wait after a failed recovery before trying again. Defaults to 30. */
|
|
24
|
+
readonly backoffFailConnectSeconds?: number
|
|
9
25
|
readonly tcp?: ITcpTransportDescription
|
|
10
26
|
readonly http?: IHttpTransportDescription
|
|
11
27
|
readonly protocol: string
|
|
@@ -631,11 +631,21 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
631
631
|
*/
|
|
632
632
|
protected abstract onStopped (error?: Error): void
|
|
633
633
|
/**
|
|
634
|
-
*
|
|
634
|
+
* Inform the application of a peer login attempt and ask it to accept or reject.
|
|
635
|
+
*
|
|
636
|
+
* Return true to let the session continue. Return false to refuse the peer: a
|
|
637
|
+
* Logout carrying a Text reason is sent and the transport is dropped, so onReady
|
|
638
|
+
* is never reached and no heartbeat is started.
|
|
639
|
+
*
|
|
640
|
+
* The verdict may be returned as a promise, for applications that must check the
|
|
641
|
+
* credentials against an async source such as an http api or a database. While
|
|
642
|
+
* that promise is pending the transport is paused and any message the peer sends
|
|
643
|
+
* is queued, then replayed in order once the verdict arrives.
|
|
644
|
+
*
|
|
635
645
|
* @param view the login message
|
|
636
646
|
* @param user extracted user from message
|
|
637
647
|
* @param password extracted password from the message.
|
|
638
648
|
* @protected
|
|
639
649
|
*/
|
|
640
|
-
protected abstract onLogon (view: MsgView, user: string, password: string): boolean
|
|
650
|
+
protected abstract onLogon (view: MsgView, user: string, password: string): boolean | Promise<boolean>
|
|
641
651
|
}
|
|
@@ -25,8 +25,14 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
25
25
|
private initiator: TcpInitiator
|
|
26
26
|
private transport: MsgTransport
|
|
27
27
|
private th: Timeout | null = null
|
|
28
|
-
|
|
29
|
-
public
|
|
28
|
+
private stopRequested: boolean = false
|
|
29
|
+
public static readonly DefaultRecoveryAttemptSecs = 5
|
|
30
|
+
public static readonly DefaultBackoffFailConnectSecs = 30
|
|
31
|
+
public static readonly DefaultConnectTimeoutSecs = 60
|
|
32
|
+
|
|
33
|
+
public recoveryAttemptSecs: number = RecoveringTcpInitiator.DefaultRecoveryAttemptSecs
|
|
34
|
+
public backoffFailConnectSecs: number = RecoveringTcpInitiator.DefaultBackoffFailConnectSecs
|
|
35
|
+
public connectTimeoutSecs: number = RecoveringTcpInitiator.DefaultConnectTimeoutSecs
|
|
30
36
|
|
|
31
37
|
constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
|
|
32
38
|
super(jsFixConfig)
|
|
@@ -40,6 +46,14 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
40
46
|
if (!this.tcp) {
|
|
41
47
|
throw new Error('no tcp in session description need tcp { host: hostname, port: port }')
|
|
42
48
|
}
|
|
49
|
+
// the retry policy used to live only in these fields, which nothing set - an
|
|
50
|
+
// application could not change how hard or how long the engine tried to get its
|
|
51
|
+
// transport back without subclassing the launcher. See issue #72.
|
|
52
|
+
this.recoveryAttemptSecs = this.application.recoveryAttemptSeconds ?? this.recoveryAttemptSecs
|
|
53
|
+
this.backoffFailConnectSecs = this.application.backoffFailConnectSeconds ?? this.backoffFailConnectSecs
|
|
54
|
+
this.connectTimeoutSecs = this.application.connectTimeoutSeconds ?? this.connectTimeoutSecs
|
|
55
|
+
this.logger.info(`recovery policy: connectTimeout=${this.connectTimeoutSecs}s, ` +
|
|
56
|
+
`recoveryAttempt=${this.recoveryAttemptSecs}s, backoffFailConnect=${this.backoffFailConnectSecs}s`)
|
|
43
57
|
this.createSession(jsFixConfig)
|
|
44
58
|
}
|
|
45
59
|
|
|
@@ -54,6 +68,13 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
54
68
|
this.logger.info('session has permanently ended')
|
|
55
69
|
this.emit('end', this)
|
|
56
70
|
})
|
|
71
|
+
// A lost transport is this initiator's normal course of events, and recovery is
|
|
72
|
+
// driven by session.run() rejecting rather than by this event. But whilst waiting
|
|
73
|
+
// between attempts there is no run in flight to carry a listener, so a stop landing
|
|
74
|
+
// then would emit 'error' to nobody - which node treats as fatal.
|
|
75
|
+
this.session.on('error', (e: Error) => {
|
|
76
|
+
this.logger.info(`session error: ${e.message}`)
|
|
77
|
+
})
|
|
57
78
|
this.session.setState(SessionState.DisconnectedNoConnectionToday)
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -104,7 +125,31 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
104
125
|
// succeed in which case can restart session or fails in which case wait and
|
|
105
126
|
// restart an attempt to connect
|
|
106
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Give up recovering and end the session for good.
|
|
130
|
+
*
|
|
131
|
+
* Without this a resilient initiator could not be shut down: losing the transport
|
|
132
|
+
* schedules another attempt, and nothing cancelled that timer, so the process stayed
|
|
133
|
+
* alive retrying a counterparty the application had finished with. Safe to call
|
|
134
|
+
* twice, and safe whilst waiting between attempts with no transport at all.
|
|
135
|
+
*/
|
|
136
|
+
public stop (): void {
|
|
137
|
+
if (this.stopRequested) return
|
|
138
|
+
this.stopRequested = true
|
|
139
|
+
this.logger.info('stop requested - cancelling recovery')
|
|
140
|
+
this.clearTimer()
|
|
141
|
+
if (this.session.getState() !== SessionState.Stopped) {
|
|
142
|
+
this.session.requestStop('initiator stopped')
|
|
143
|
+
}
|
|
144
|
+
this.emit('end', this)
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
private recover (): void {
|
|
148
|
+
if (this.stopRequested) {
|
|
149
|
+
this.logger.info('transport lost after a stop was requested - not recovering')
|
|
150
|
+
this.emit('end', this)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
108
153
|
this.session.setState(SessionState.DetectBrokenNetworkConnection)
|
|
109
154
|
this.logger.info(`recover session transport - attempt in ${this.recoveryAttemptSecs} secs`)
|
|
110
155
|
if (!this.resetSeq()) {
|
|
@@ -115,7 +160,7 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
115
160
|
}
|
|
116
161
|
}
|
|
117
162
|
this.th = setTimeout(() => {
|
|
118
|
-
this.connect(
|
|
163
|
+
this.connect(this.connectTimeoutSecs).then(t => {
|
|
119
164
|
this.logger.info(`new transport ${t.id}`)
|
|
120
165
|
}).catch((e) => {
|
|
121
166
|
this.logger.info(`failed to re-connect ${e.message} - backoff for ${this.backoffFailConnectSecs}`)
|
|
@@ -135,18 +180,34 @@ export class RecoveringTcpInitiator extends FixEntity {
|
|
|
135
180
|
|
|
136
181
|
// for first connection - reject if no initial connection established within timeout
|
|
137
182
|
// once connection established, will not resolve until session is ended - i.e. lost
|
|
138
|
-
// connections are re-established using the same session instance.
|
|
183
|
+
// connections are re-established using the same session instance. So this promise
|
|
184
|
+
// is the lifetime of the application, not a signal that the session is up: for that,
|
|
185
|
+
// use onReady on the session. See issue #72.
|
|
139
186
|
|
|
140
|
-
public async run (initialTimeout: number =
|
|
187
|
+
public async run (initialTimeout: number = this.connectTimeoutSecs): Promise<any> {
|
|
141
188
|
return await new Promise<any>((resolve, reject) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
189
|
+
// 'end' can arrive more than once - a stop both ends the session and calls off
|
|
190
|
+
// recovery, and each path announces itself - so settle only on the first. The
|
|
191
|
+
// listener goes on before connecting, not after: a stop landing whilst the first
|
|
192
|
+
// connection is still being attempted has to be able to end this too, and until
|
|
193
|
+
// now there was nothing listening that early to hear it.
|
|
194
|
+
let ended = false
|
|
195
|
+
const finish = (): void => {
|
|
196
|
+
if (ended) return
|
|
197
|
+
ended = true
|
|
198
|
+
this.clearTimer()
|
|
199
|
+
this.initiator?.end()
|
|
200
|
+
this.logger.info(`run: transport ${this.transport?.id ?? 'none'} ends - resolving`)
|
|
201
|
+
resolve(null)
|
|
202
|
+
}
|
|
203
|
+
this.on('end', finish)
|
|
204
|
+
|
|
205
|
+
this.connect(initialTimeout).catch(e => {
|
|
206
|
+
if (this.stopRequested) {
|
|
207
|
+
this.logger.info('run: first connection abandoned on stop - resolving')
|
|
208
|
+
finish()
|
|
209
|
+
return
|
|
210
|
+
}
|
|
150
211
|
this.logger.info(`run: failed to connect to first transport ${initialTimeout} - rejecting`)
|
|
151
212
|
reject(e)
|
|
152
213
|
})
|
|
@@ -9,8 +9,13 @@ import { FixEntity } from '../fix-entity'
|
|
|
9
9
|
|
|
10
10
|
@injectable()
|
|
11
11
|
export class TcpInitiatorConnector extends FixEntity {
|
|
12
|
+
public static readonly DefaultConnectTimeoutSecs = 22
|
|
13
|
+
|
|
14
|
+
public connectTimeoutSecs: number = TcpInitiatorConnector.DefaultConnectTimeoutSecs
|
|
15
|
+
|
|
12
16
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
13
17
|
super(config)
|
|
18
|
+
this.connectTimeoutSecs = config.description.application?.connectTimeoutSeconds ?? this.connectTimeoutSecs
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
async start (reconnectTimeout: number = 0): Promise<any> {
|
|
@@ -62,7 +67,7 @@ export class TcpInitiatorConnector extends FixEntity {
|
|
|
62
67
|
const logger = this.config.logFactory.logger('initiator')
|
|
63
68
|
const initiator: FixInitiator = this.config.sessionContainer.resolve<FixInitiator>(TcpInitiator)
|
|
64
69
|
logger.info('connecting ...')
|
|
65
|
-
const initiatorTransport: MsgTransport = await initiator.connect(
|
|
70
|
+
const initiatorTransport: MsgTransport = await initiator.connect(this.connectTimeoutSecs)
|
|
66
71
|
logger.info('... connected, run session')
|
|
67
72
|
await initiatorSession.run(initiatorTransport)
|
|
68
73
|
logger.info('ends')
|
|
@@ -28,6 +28,7 @@ export class TcpInitiator extends FixInitiator {
|
|
|
28
28
|
private readonly logger: IJsFixLogger
|
|
29
29
|
private duplex: FixDuplex
|
|
30
30
|
private th: Timeout | null = null
|
|
31
|
+
private timeoutTh: Timeout | null = null
|
|
31
32
|
|
|
32
33
|
constructor (@inject(DITokens.IJsFixConfig) public readonly jsFixConfig: IJsFixConfig) {
|
|
33
34
|
super(jsFixConfig.description.application ?? null)
|
|
@@ -189,13 +190,15 @@ export class TcpInitiator extends FixInitiator {
|
|
|
189
190
|
clearInterval(this.th)
|
|
190
191
|
this.th = null
|
|
191
192
|
}
|
|
193
|
+
if (this.timeoutTh) {
|
|
194
|
+
clearTimeout(this.timeoutTh)
|
|
195
|
+
this.timeoutTh = null
|
|
196
|
+
}
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
private async repeatConnect (timeoutSeconds: number, initialError?: Error): Promise < MsgTransport > {
|
|
195
200
|
return await new Promise<MsgTransport>(async (resolve, reject) => {
|
|
196
201
|
const application = this.application
|
|
197
|
-
const promisify = util.promisify
|
|
198
|
-
const timeoutPromise = promisify(setTimeout)
|
|
199
202
|
const reconnectSeconds = application?.reconnectSeconds ?? 5
|
|
200
203
|
let retries = 0
|
|
201
204
|
let lastError: Error | undefined = initialError
|
|
@@ -212,15 +215,18 @@ export class TcpInitiator extends FixInitiator {
|
|
|
212
215
|
this.logger.info(`${name}: retries ${retries} ${e.message}`)
|
|
213
216
|
})
|
|
214
217
|
}, reconnectSeconds * 1000)
|
|
215
|
-
|
|
218
|
+
// a plain timer, not a promisified one: promisify(setTimeout) hands back no
|
|
219
|
+
// handle, so this deadline could never be cancelled. Connecting successfully
|
|
220
|
+
// cleared the retry interval and left this one running to its full term - up to
|
|
221
|
+
// the 60s an initiator allows - holding the event loop open long after the
|
|
222
|
+
// session was up, and stopping a process (or a test runner) from exiting.
|
|
223
|
+
this.timeoutTh = setTimeout(() => {
|
|
216
224
|
this.clearTimer()
|
|
217
225
|
this.state = InitiatorState.Stopped
|
|
218
226
|
const e = lastError ?? new Error(`${name}: timeout of ${timeoutSeconds} whilst connecting`)
|
|
219
227
|
this.logger.warning(`${name}: giving up after ${timeoutSeconds}s and ${retries} retries, last error: ${e.message}`)
|
|
220
228
|
reject(e)
|
|
221
|
-
}
|
|
222
|
-
reject(e)
|
|
223
|
-
})
|
|
229
|
+
}, timeoutSeconds * 1000)
|
|
224
230
|
})
|
|
225
231
|
}
|
|
226
232
|
}
|