jspurefix 5.8.5 → 5.9.2
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/.gitattributes +4 -0
- package/BACKPORT_PLAN.md +84 -0
- package/DEMO_PORT_PLAN.md +10 -0
- package/README.md +3 -0
- package/WORKLOG.md +119 -0
- package/dist/benchmark/bench-report.d.ts +5 -0
- package/dist/benchmark/bench-report.js +91 -0
- package/dist/benchmark/bench-report.js.map +1 -0
- package/dist/benchmark/bench-runner.d.ts +12 -0
- package/dist/benchmark/bench-runner.js +193 -0
- package/dist/benchmark/bench-runner.js.map +1 -0
- package/dist/benchmark/bench-stats.d.ts +10 -0
- package/dist/benchmark/bench-stats.js +56 -0
- package/dist/benchmark/bench-stats.js.map +1 -0
- package/dist/benchmark/bench-types.d.ts +74 -0
- package/dist/benchmark/bench-types.js +3 -0
- package/dist/benchmark/bench-types.js.map +1 -0
- package/dist/benchmark/gc-probe.d.ts +18 -0
- package/dist/benchmark/gc-probe.js +103 -0
- package/dist/benchmark/gc-probe.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.js +23 -0
- package/dist/benchmark/index.js.map +1 -0
- package/dist/benchmark/parse-bench.d.ts +28 -0
- package/dist/benchmark/parse-bench.js +85 -0
- package/dist/benchmark/parse-bench.js.map +1 -0
- package/dist/buffer/ascii/ascii-parser.d.ts +1 -0
- package/dist/buffer/ascii/ascii-parser.js +4 -1
- package/dist/buffer/ascii/ascii-parser.js.map +1 -1
- package/dist/buffer/ascii/ascii-segment-parser.js +3 -2
- package/dist/buffer/ascii/ascii-segment-parser.js.map +1 -1
- package/dist/buffer/ascii/tag-index.js.map +1 -1
- package/dist/buffer/msg-view.d.ts +1 -0
- package/dist/buffer/msg-view.js +25 -19
- package/dist/buffer/msg-view.js.map +1 -1
- package/dist/config/js-fix-config.d.ts +4 -0
- package/dist/config/js-fix-config.js.map +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/dictionary-validator.js.map +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.d.ts +1 -1
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js +1 -2
- package/dist/dictionary/parser/quickfix/quick-fix-graph-parser.js.map +1 -1
- package/dist/dictionary/parser/quickfix/validation-error.d.ts +2 -2
- package/dist/dictionary/parser/quickfix/validation-error.js.map +1 -1
- package/dist/jsfix-bench.d.ts +1 -0
- package/dist/jsfix-bench.js +213 -0
- package/dist/jsfix-bench.js.map +1 -0
- package/dist/runtime/session-launcher.js +1 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/runtime/session-scope.d.ts +3 -0
- package/dist/runtime/session-scope.js +47 -0
- package/dist/runtime/session-scope.js.map +1 -0
- package/dist/sample/http/oms/app.js +3 -3
- package/dist/sample/http/oms/app.js.map +1 -1
- package/dist/sample/tcp/qf-md/app.js +3 -3
- package/dist/sample/tcp/qf-md/app.js.map +1 -1
- package/dist/sample/tcp/skeleton/app.js +1 -1
- package/dist/sample/tcp/skeleton/app.js.map +1 -1
- package/dist/sample/tcp/trade-capture/app-launcher.js +3 -3
- package/dist/sample/tcp/trade-capture/app-launcher.js.map +1 -1
- package/dist/store/file-session-store.js +1 -1
- package/dist/store/file-session-store.js.map +1 -1
- package/dist/store/file-session-stream-provider.d.ts +1 -0
- package/dist/store/file-session-stream-provider.js +13 -13
- package/dist/store/file-session-stream-provider.js.map +1 -1
- package/dist/store/fix-session-store-factory.d.ts +1 -1
- package/dist/store/fix-session-store-factory.js.map +1 -1
- package/dist/store/fix-session-store.d.ts +11 -11
- package/dist/store/fix-session-store.js.map +1 -1
- package/dist/store/memory-session-store.js.map +1 -1
- package/dist/store/session-stream-provider.d.ts +13 -13
- package/dist/store/session-stream-provider.js.map +1 -1
- package/dist/transport/ascii/ascii-session.d.ts +17 -3
- package/dist/transport/ascii/ascii-session.js +121 -8
- package/dist/transport/ascii/ascii-session.js.map +1 -1
- package/dist/transport/duplex/fix-duplex.d.ts +1 -0
- package/dist/transport/duplex/fix-duplex.js +3 -0
- package/dist/transport/duplex/fix-duplex.js.map +1 -1
- package/dist/transport/duplex/tcp-duplex.d.ts +1 -0
- package/dist/transport/duplex/tcp-duplex.js +5 -0
- package/dist/transport/duplex/tcp-duplex.js.map +1 -1
- package/dist/transport/factory/msg-transport.d.ts +2 -0
- package/dist/transport/factory/msg-transport.js +17 -2
- package/dist/transport/factory/msg-transport.js.map +1 -1
- package/dist/transport/session/a-session-msg-factory.d.ts +1 -0
- package/dist/transport/session/a-session-msg-factory.js +4 -0
- package/dist/transport/session/a-session-msg-factory.js.map +1 -1
- package/dist/transport/session/clone-session-description.d.ts +6 -0
- package/dist/transport/session/clone-session-description.js +11 -0
- package/dist/transport/session/clone-session-description.js.map +1 -0
- package/dist/transport/session/fix-clock.d.ts +1 -1
- package/dist/transport/session/fix-clock.js.map +1 -1
- package/dist/transport/session/fix-session.d.ts +5 -1
- package/dist/transport/session/fix-session.js +93 -24
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/index.d.ts +2 -0
- package/dist/transport/session/index.js +2 -0
- package/dist/transport/session/index.js.map +1 -1
- package/dist/transport/session/resend-request-manager.d.ts +2 -2
- package/dist/transport/session/resend-request-manager.js.map +1 -1
- package/dist/transport/session/session-msg-factory.d.ts +1 -0
- package/dist/transport/session/session-msg-factory.js.map +1 -1
- package/dist/transport/session/session-registry.d.ts +23 -0
- package/dist/transport/session/session-registry.js +59 -0
- package/dist/transport/session/session-registry.js.map +1 -0
- package/dist/transport/session/session-sequence-coordinator.d.ts +1 -1
- package/dist/transport/session/session-sequence-coordinator.js.map +1 -1
- package/dist/transport/session/session-sequence-store.d.ts +3 -3
- package/dist/transport/session/session-sequence-store.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor-listener.d.ts +3 -0
- package/dist/transport/tcp/tcp-acceptor-listener.js +26 -4
- package/dist/transport/tcp/tcp-acceptor-listener.js.map +1 -1
- package/dist/transport/tcp/tcp-acceptor.d.ts +5 -0
- package/dist/transport/tcp/tcp-acceptor.js +74 -20
- package/dist/transport/tcp/tcp-acceptor.js.map +1 -1
- package/dist/transport/tcp/tcp-initiator.js +15 -5
- package/dist/transport/tcp/tcp-initiator.js.map +1 -1
- package/dist/transport/tcp/tcp-transport-description.d.ts +1 -0
- package/dist/transport/tcp/tcp-transport-description.js.map +1 -1
- package/dist/transport/tcp/tls-diagnostics.d.ts +5 -0
- package/dist/transport/tcp/tls-diagnostics.js +64 -0
- package/dist/transport/tcp/tls-diagnostics.js.map +1 -0
- package/dist/transport/tcp/tls-options-factory.d.ts +1 -0
- package/dist/transport/tcp/tls-options-factory.js +24 -2
- package/dist/transport/tcp/tls-options-factory.js.map +1 -1
- package/docs/acceptor.md +207 -0
- package/docs/benchmarking.md +117 -0
- package/eslint.config.mjs +15 -1
- package/jsfix.test_client.txt +62 -75
- package/jsfix.test_server.txt +72 -72
- package/package.json +19 -10
- package/script/pack-local.js +68 -0
- package/src/benchmark/bench-report.ts +101 -0
- package/src/benchmark/bench-runner.ts +196 -0
- package/src/benchmark/bench-stats.ts +86 -0
- package/src/benchmark/bench-types.ts +110 -0
- package/src/benchmark/gc-probe.ts +145 -0
- package/src/benchmark/index.ts +6 -0
- package/src/benchmark/parse-bench.ts +144 -0
- package/src/buffer/ascii/ascii-parser.ts +16 -1
- package/src/buffer/ascii/ascii-segment-parser.ts +9 -6
- package/src/buffer/ascii/tag-index.ts +3 -3
- package/src/buffer/msg-view.ts +35 -18
- package/src/config/js-fix-config.ts +8 -0
- package/src/dictionary/parser/quickfix/dictionary-validator.ts +1 -1
- package/src/dictionary/parser/quickfix/quick-fix-graph-parser.ts +2 -3
- package/src/dictionary/parser/quickfix/validation-error.ts +1 -1
- package/src/jsfix-bench.ts +204 -0
- package/src/runtime/session-launcher.ts +5 -1
- package/src/runtime/session-scope.ts +84 -0
- package/src/sample/http/oms/app.ts +3 -3
- package/src/sample/tcp/qf-md/app.ts +3 -3
- package/src/sample/tcp/skeleton/app.ts +1 -1
- package/src/sample/tcp/trade-capture/app-launcher.ts +3 -3
- package/src/store/file-session-store.ts +2 -2
- package/src/store/file-session-stream-provider.ts +17 -11
- package/src/store/fix-session-store-factory.ts +1 -1
- package/src/store/fix-session-store.ts +11 -11
- package/src/store/memory-session-store.ts +1 -1
- package/src/store/session-stream-provider.ts +13 -13
- package/src/transport/ascii/ascii-session.ts +186 -11
- package/src/transport/duplex/fix-duplex.ts +9 -0
- package/src/transport/duplex/tcp-duplex.ts +10 -0
- package/src/transport/factory/msg-transport.ts +20 -0
- package/src/transport/session/a-session-msg-factory.ts +18 -0
- package/src/transport/session/clone-session-description.ts +41 -0
- package/src/transport/session/fix-clock.ts +1 -1
- package/src/transport/session/fix-session.ts +136 -25
- package/src/transport/session/index.ts +2 -0
- package/src/transport/session/resend-request-manager.ts +4 -4
- package/src/transport/session/session-msg-factory.ts +13 -0
- package/src/transport/session/session-registry.ts +112 -0
- package/src/transport/session/session-sequence-coordinator.ts +1 -1
- package/src/transport/session/session-sequence-store.ts +3 -3
- package/src/transport/tcp/tcp-acceptor-listener.ts +43 -5
- package/src/transport/tcp/tcp-acceptor.ts +92 -18
- package/src/transport/tcp/tcp-initiator.ts +26 -5
- package/src/transport/tcp/tcp-transport-description.ts +7 -0
- package/src/transport/tcp/tls-diagnostics.ts +76 -0
- package/src/transport/tcp/tls-options-factory.ts +34 -2
|
@@ -9,17 +9,44 @@ import * as events from 'events'
|
|
|
9
9
|
import { SessionState } from './session-state'
|
|
10
10
|
import { SegmentType } from '../../buffer/segment/segment-type'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The set of listeners this session attached to one specific transport.
|
|
14
|
+
*
|
|
15
|
+
* The handlers must be kept as stable references - `EventEmitter.removeListener`
|
|
16
|
+
* matches on identity, so subscribing with a fresh closure and unsubscribing with
|
|
17
|
+
* a method reference silently leaves the listener attached (issue #153). Holding
|
|
18
|
+
* the transport alongside the handlers also lets `unsubscribe` detach from the
|
|
19
|
+
* transport it actually subscribed to, even when `this.transport` has already
|
|
20
|
+
* moved on to a newer connection.
|
|
21
|
+
*/
|
|
22
|
+
interface ITransportSubscription {
|
|
23
|
+
readonly transport: MsgTransport
|
|
24
|
+
readonly rxOnMsg: (msgType: string, view: MsgView) => void
|
|
25
|
+
readonly rxOnError: (e: Error) => void
|
|
26
|
+
readonly rxOnDone: () => void
|
|
27
|
+
readonly rxOnEnd: () => void
|
|
28
|
+
readonly rxOnDecoded: (msgType: string, data: ElasticBuffer, ptr: number) => void
|
|
29
|
+
readonly txOnError: (e: Error) => void
|
|
30
|
+
readonly txOnEncoded: (msgType: string, data: string, hdr: ILooseObject) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export abstract class FixSession extends events.EventEmitter {
|
|
13
34
|
public logReceivedMsgs: boolean = false
|
|
14
35
|
protected timer: NodeJS.Timeout | null = null
|
|
15
36
|
protected transport: MsgTransport | null = null
|
|
37
|
+
private subscription: ITransportSubscription | null = null
|
|
16
38
|
public manageSession: boolean = true
|
|
17
39
|
public checkMsgIntegrity: boolean = false
|
|
18
40
|
protected readonly me: string
|
|
19
41
|
protected readonly initiator: boolean
|
|
20
42
|
protected readonly acceptor: boolean
|
|
21
43
|
protected readonly sessionState: FixSessionState
|
|
22
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Not readonly: a wildcard acceptor does not know which counterparty it is
|
|
46
|
+
* serving until the Logon arrives, and every session on that listener would
|
|
47
|
+
* otherwise log under the same name. Rebound once the identity is known.
|
|
48
|
+
*/
|
|
49
|
+
protected sessionLogger: IJsFixLogger
|
|
23
50
|
protected requestLogoutType: string
|
|
24
51
|
protected respondLogoutType: string
|
|
25
52
|
protected requestLogonType: string
|
|
@@ -120,7 +147,9 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
120
147
|
}
|
|
121
148
|
const onError = (e: Error): void => {
|
|
122
149
|
cleanup()
|
|
123
|
-
logger
|
|
150
|
+
// read the logger now, not the one captured when the run started - a
|
|
151
|
+
// wildcard acceptor rebinds it once it knows which peer it is serving
|
|
152
|
+
this.sessionLogger.error(e)
|
|
124
153
|
reject(e)
|
|
125
154
|
}
|
|
126
155
|
const onDone = (): void => {
|
|
@@ -217,35 +246,90 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
217
246
|
this.onEncoded(msgType, data)
|
|
218
247
|
}
|
|
219
248
|
|
|
249
|
+
/**
|
|
250
|
+
* true when the event which has just fired came from the transport this session
|
|
251
|
+
* is currently running. A half open socket can keep emitting long after the peer
|
|
252
|
+
* has reconnected on a fresh connection - those events must not be allowed to drive
|
|
253
|
+
* the live session (issue #153).
|
|
254
|
+
*/
|
|
255
|
+
private isCurrentTransport (transport: MsgTransport, event: string): boolean {
|
|
256
|
+
if (this.subscription?.transport === transport && this.transport === transport) {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
const current = this.transport?.id ?? 'none'
|
|
260
|
+
this.sessionLogger.warning(`ignoring '${event}' from stale transport ${transport.id} - current transport is ${current}`)
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
220
264
|
protected unsubscribe (): void {
|
|
221
265
|
const logger = this.sessionLogger
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
rx
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
266
|
+
const subscription = this.subscription
|
|
267
|
+
if (!subscription) {
|
|
268
|
+
logger.debug('unsubscribe called with no active subscription')
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
const transport = subscription.transport
|
|
272
|
+
logger.info(`unsubscribe transport ${transport.id} sessionState = [${this.sessionState.toString()}]`)
|
|
273
|
+
const rx = transport.receiver
|
|
274
|
+
const tx = transport.transmitter
|
|
275
|
+
|
|
276
|
+
rx?.removeListener('msg', subscription.rxOnMsg)
|
|
277
|
+
rx?.removeListener('error', subscription.rxOnError)
|
|
278
|
+
rx?.removeListener('done', subscription.rxOnDone)
|
|
279
|
+
rx?.removeListener('end', subscription.rxOnEnd)
|
|
280
|
+
rx?.removeListener('decoded', subscription.rxOnDecoded)
|
|
281
|
+
tx?.removeListener('error', subscription.txOnError)
|
|
282
|
+
tx?.removeListener('encoded', subscription.txOnEncoded)
|
|
283
|
+
this.subscription = null
|
|
284
|
+
logger.debug(`unsubscribe complete - rx listeners now ${rx?.listenerCount('msg') ?? 0} msg, ${rx?.listenerCount('end') ?? 0} end`)
|
|
234
285
|
}
|
|
235
286
|
|
|
236
287
|
protected subscribe (): void {
|
|
237
288
|
const transport = this.transport
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
289
|
+
if (!transport) {
|
|
290
|
+
this.sessionLogger.warning('subscribe called with no transport')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
// detach from any previous transport before attaching to this one
|
|
294
|
+
if (this.subscription) {
|
|
295
|
+
this.unsubscribe()
|
|
296
|
+
}
|
|
241
297
|
const inst = this
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
const subscription: ITransportSubscription = {
|
|
299
|
+
transport,
|
|
300
|
+
rxOnMsg: (msgType: string, view: MsgView) => {
|
|
301
|
+
if (inst.isCurrentTransport(transport, 'msg')) inst.rxOnMsg(msgType, view)
|
|
302
|
+
},
|
|
303
|
+
rxOnError: (e: Error) => {
|
|
304
|
+
if (inst.isCurrentTransport(transport, 'error')) inst.rxOnError(e)
|
|
305
|
+
},
|
|
306
|
+
rxOnDone: () => {
|
|
307
|
+
if (inst.isCurrentTransport(transport, 'done')) inst.rxOnDone()
|
|
308
|
+
},
|
|
309
|
+
rxOnEnd: () => {
|
|
310
|
+
if (inst.isCurrentTransport(transport, 'end')) inst.rxOnEnd()
|
|
311
|
+
},
|
|
312
|
+
rxOnDecoded: (msgType: string, data: ElasticBuffer, ptr: number) => {
|
|
313
|
+
if (inst.isCurrentTransport(transport, 'decoded')) inst.rxOnDecoded(msgType, data, ptr)
|
|
314
|
+
},
|
|
315
|
+
txOnError: (e: Error) => {
|
|
316
|
+
if (inst.isCurrentTransport(transport, 'tx error')) inst.txOnError(e)
|
|
317
|
+
},
|
|
318
|
+
txOnEncoded: (msgType: string, data: string, hdr: ILooseObject) => {
|
|
319
|
+
if (inst.isCurrentTransport(transport, 'encoded')) inst.txOnEncoded(msgType, data, hdr)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const rx = transport.receiver
|
|
323
|
+
const tx = transport.transmitter
|
|
324
|
+
rx?.on('msg', subscription.rxOnMsg)
|
|
325
|
+
rx?.on('error', subscription.rxOnError)
|
|
326
|
+
rx?.on('done', subscription.rxOnDone)
|
|
327
|
+
rx?.on('end', subscription.rxOnEnd)
|
|
328
|
+
rx?.on('decoded', subscription.rxOnDecoded)
|
|
329
|
+
tx?.on('error', subscription.txOnError)
|
|
330
|
+
tx?.on('encoded', subscription.txOnEncoded)
|
|
331
|
+
this.subscription = subscription
|
|
332
|
+
this.sessionLogger.info(`subscribed to transport ${transport.id}`)
|
|
249
333
|
}
|
|
250
334
|
|
|
251
335
|
protected validStateApplicationMsg (): boolean {
|
|
@@ -285,12 +369,16 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
285
369
|
if (this.sessionState.state === SessionState.Stopped) return
|
|
286
370
|
this.sessionLogger.error(error)
|
|
287
371
|
this.stopTimer()
|
|
372
|
+
this.unsubscribe()
|
|
288
373
|
if (this.transport) {
|
|
374
|
+
this.sessionLogger.info(`terminate: kill transport ${this.transport.id}`)
|
|
289
375
|
this.transport.end()
|
|
290
376
|
}
|
|
291
377
|
this.transport = null
|
|
292
378
|
this.setState(SessionState.Stopped)
|
|
379
|
+
this.onSessionStopping()
|
|
293
380
|
this.emit('error', error)
|
|
381
|
+
this.onStopped(error)
|
|
294
382
|
}
|
|
295
383
|
|
|
296
384
|
protected peerLogout (view: MsgView): void {
|
|
@@ -415,6 +503,16 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
415
503
|
// Override in subclass to reset coordinator/transient state
|
|
416
504
|
}
|
|
417
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Called once as the session moves to Stopped, before the application is informed
|
|
508
|
+
* via onStopped. Override to release session scoped resources - the ascii session
|
|
509
|
+
* uses this to remove itself from the session registry. Mirrors cspurefix
|
|
510
|
+
* FixSession.OnSessionStopping.
|
|
511
|
+
*/
|
|
512
|
+
protected onSessionStopping (): void {
|
|
513
|
+
// Default no-op
|
|
514
|
+
}
|
|
515
|
+
|
|
418
516
|
/**
|
|
419
517
|
* Called for initiators after the store is loaded but before the Logon is sent.
|
|
420
518
|
* Override to reset sequences/store when ResetSeqNumFlag=Y so the Logon goes out
|
|
@@ -424,13 +522,26 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
424
522
|
// Default no-op
|
|
425
523
|
}
|
|
426
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Stop this session on behalf of an external caller - typically the session registry
|
|
527
|
+
* when a new connection for the same SessionId replaces this one, but equally an
|
|
528
|
+
* application which has decided the peer is gone. Mirrors cspurefix
|
|
529
|
+
* FixSession.RequestStop.
|
|
530
|
+
* @param reason recorded in the session log and carried on the emitted error
|
|
531
|
+
*/
|
|
532
|
+
public requestStop (reason: string): void {
|
|
533
|
+
this.sessionLogger.info(`requestStop: ${reason}`)
|
|
534
|
+
this.stop(new Error(reason))
|
|
535
|
+
}
|
|
536
|
+
|
|
427
537
|
protected stop (error: Error | null = null): void {
|
|
428
538
|
if (this.sessionState.state === SessionState.Stopped) {
|
|
429
539
|
return
|
|
430
540
|
}
|
|
431
541
|
this.stopTimer()
|
|
432
542
|
this.unsubscribe()
|
|
433
|
-
this.sessionLogger.info(
|
|
543
|
+
this.sessionLogger.info(`stop: kill transport ${this.transport?.id ?? 'none'}`)
|
|
544
|
+
this.onSessionStopping()
|
|
434
545
|
this.transport?.end()
|
|
435
546
|
if (error) {
|
|
436
547
|
this.sessionLogger.info(`stop: emit error ${error.message}`)
|
|
@@ -46,7 +46,7 @@ export class ResendAction {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export class PendingResendRange {
|
|
49
|
-
private readonly receivedSeqs
|
|
49
|
+
private readonly receivedSeqs = new Set<number>()
|
|
50
50
|
|
|
51
51
|
constructor (
|
|
52
52
|
public readonly begin: number,
|
|
@@ -190,11 +190,11 @@ export class ResendRequestManager {
|
|
|
190
190
|
// keep requestHistory for debugging
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
get pending ():
|
|
193
|
+
get pending (): readonly PendingResendRange[] {
|
|
194
194
|
return this.pendingRequests.slice()
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
get history ():
|
|
197
|
+
get history (): readonly ResendRequestRecord[] {
|
|
198
198
|
return this.requestHistory.slice()
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -240,7 +240,7 @@ export class ResendRequestManager {
|
|
|
240
240
|
return { begin: gapBegin, end: gapEnd }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const covered
|
|
243
|
+
const covered = new Set<number>()
|
|
244
244
|
for (const pending of this.pendingRequests) {
|
|
245
245
|
for (let seq = pending.begin; seq <= pending.end; seq++) {
|
|
246
246
|
covered.add(seq)
|
|
@@ -13,4 +13,17 @@ export interface ISessionMsgFactory {
|
|
|
13
13
|
heartbeat: (testReqId: string) => ILooseObject
|
|
14
14
|
header: (msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>) => ILooseObject
|
|
15
15
|
trailer: (checksum: number) => ILooseObject
|
|
16
|
+
/**
|
|
17
|
+
* Produce a factory of this same kind bound to another session description.
|
|
18
|
+
*
|
|
19
|
+
* An acceptor builds one of these per accepted connection, because the outbound
|
|
20
|
+
* header carries that session's comp ids - and under a wildcard TargetCompID the
|
|
21
|
+
* target is not known until the peer logs on. Implementing this is what lets an
|
|
22
|
+
* application's own factory survive into those per-session scopes; without it the
|
|
23
|
+
* acceptor can only fall back to the stock factory for the protocol.
|
|
24
|
+
*
|
|
25
|
+
* ASessionMsgFactory supplies a default that reconstructs via the concrete
|
|
26
|
+
* constructor, so a subclass taking (description, mutator) needs to do nothing.
|
|
27
|
+
*/
|
|
28
|
+
cloneFor?: (description: ISessionDescription) => ISessionMsgFactory
|
|
16
29
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SessionId } from '../../store/session-id'
|
|
2
|
+
import { IJsFixLogger } from '../../config/js-fix-logger'
|
|
3
|
+
import { JsFixLoggerFactory } from '../../config/js-fix-logger-factory'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The registry only ever needs to ask a session to stop, so it is typed against
|
|
7
|
+
* that capability rather than against FixSession. IJsFixConfig carries a registry,
|
|
8
|
+
* and FixSession is constructed from IJsFixConfig - depending on the concrete class
|
|
9
|
+
* here would close an import cycle.
|
|
10
|
+
*/
|
|
11
|
+
export interface IStoppableSession {
|
|
12
|
+
requestStop: (reason: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tracks the live session for each SessionId served by an acceptor.
|
|
17
|
+
*
|
|
18
|
+
* FIX allows exactly one active session per (BeginString, SenderCompID,
|
|
19
|
+
* TargetCompID). A counterparty whose socket has gone half open - no FIN, no RST,
|
|
20
|
+
* so node never raises 'end' - will happily open a second connection and log on
|
|
21
|
+
* again. Without a registry the acceptor then runs two sessions for one CompID
|
|
22
|
+
* pair, both writing to the same message store and both stamping sequence numbers,
|
|
23
|
+
* which corrupts the store and produces sequence errors the peer cannot recover
|
|
24
|
+
* from. See https://github.com/TimelordUK/jspurefix/issues/153.
|
|
25
|
+
*
|
|
26
|
+
* Ported from cspurefix PureFix.Transport/Session/SessionRegistry.cs.
|
|
27
|
+
*/
|
|
28
|
+
export interface ISessionRegistry {
|
|
29
|
+
/**
|
|
30
|
+
* Claim this SessionId for the given session. Any *different* session already
|
|
31
|
+
* holding it is stopped first, because its transport is by definition stale.
|
|
32
|
+
* @returns true when an existing session was stopped
|
|
33
|
+
*/
|
|
34
|
+
register: (sessionId: SessionId, session: IStoppableSession) => boolean
|
|
35
|
+
/**
|
|
36
|
+
* Release this SessionId. A no-op unless the registered session is this exact
|
|
37
|
+
* instance - a session which has already been replaced must not evict its
|
|
38
|
+
* successor on the way out.
|
|
39
|
+
*/
|
|
40
|
+
unregister: (sessionId: SessionId, session: IStoppableSession) => void
|
|
41
|
+
/** number of live sessions */
|
|
42
|
+
readonly count: number
|
|
43
|
+
/** the SessionId keys currently held, for logging and diagnostics */
|
|
44
|
+
keys: () => string[]
|
|
45
|
+
get: (sessionId: SessionId) => IStoppableSession | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const REPLACED_BY_NEW_CONNECTION =
|
|
49
|
+
'replaced by new connection - stopping old session with stale transport'
|
|
50
|
+
|
|
51
|
+
export class SessionRegistry implements ISessionRegistry {
|
|
52
|
+
private readonly sessions = new Map<string, IStoppableSession>()
|
|
53
|
+
private readonly logger: IJsFixLogger | null
|
|
54
|
+
|
|
55
|
+
constructor (logFactory?: JsFixLoggerFactory) {
|
|
56
|
+
this.logger = logFactory?.logger('SessionRegistry') ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public get count (): number {
|
|
60
|
+
return this.sessions.size
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public keys (): string[] {
|
|
64
|
+
return Array.from(this.sessions.keys())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public get (sessionId: SessionId): IStoppableSession | null {
|
|
68
|
+
return this.sessions.get(sessionId.toString()) ?? null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public register (sessionId: SessionId, session: IStoppableSession): boolean {
|
|
72
|
+
const key = sessionId.toString()
|
|
73
|
+
const logger = this.logger
|
|
74
|
+
logger?.info(`register called for SessionId=${key}, activeSessions=${this.sessions.size}`)
|
|
75
|
+
|
|
76
|
+
const existing = this.sessions.get(key)
|
|
77
|
+
let stoppedOld = false
|
|
78
|
+
|
|
79
|
+
if (existing && existing !== session) {
|
|
80
|
+
logger?.info(`FOUND EXISTING SESSION for ${key} - this is a reconnection scenario`)
|
|
81
|
+
logger?.info('stopping old session to prevent stale transport writes')
|
|
82
|
+
this.sessions.set(key, session)
|
|
83
|
+
// stop after re-pointing the map so the old session's unregister (which runs
|
|
84
|
+
// synchronously inside requestStop) sees it is no longer the holder
|
|
85
|
+
existing.requestStop(REPLACED_BY_NEW_CONNECTION)
|
|
86
|
+
stoppedOld = true
|
|
87
|
+
logger?.info('old session stopped, new session registered')
|
|
88
|
+
} else if (existing === session) {
|
|
89
|
+
logger?.debug(`same session instance already registered for ${key}`)
|
|
90
|
+
} else {
|
|
91
|
+
logger?.info(`no existing session found - registering new session: ${key}`)
|
|
92
|
+
this.sessions.set(key, session)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
logger?.info(`register complete for ${key}: stoppedOldSession=${stoppedOld}, totalActiveSessions=${this.sessions.size}`)
|
|
96
|
+
return stoppedOld
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public unregister (sessionId: SessionId, session: IStoppableSession): void {
|
|
100
|
+
const key = sessionId.toString()
|
|
101
|
+
const logger = this.logger
|
|
102
|
+
logger?.info(`unregister called for SessionId=${key}`)
|
|
103
|
+
|
|
104
|
+
if (this.sessions.get(key) === session) {
|
|
105
|
+
this.sessions.delete(key)
|
|
106
|
+
logger?.info(`successfully unregistered session: ${key}, remainingActiveSessions=${this.sessions.size}`)
|
|
107
|
+
} else {
|
|
108
|
+
// expected when a new connection has already taken this SessionId over
|
|
109
|
+
logger?.info(`session NOT unregistered (already replaced by new connection or not found): ${key}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -160,7 +160,7 @@ export class SessionSequenceCoordinator {
|
|
|
160
160
|
this.resendManager.recordRequestSent(begin, end, now)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
get pendingResendRequests ():
|
|
163
|
+
get pendingResendRequests (): readonly PendingResendRange[] {
|
|
164
164
|
return this.resendManager.pending
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
export interface ISessionSequenceStore {
|
|
7
7
|
senderSeqNum: number
|
|
8
8
|
targetSeqNum: number
|
|
9
|
-
setSenderSeqNum (value: number)
|
|
10
|
-
setTargetSeqNum (value: number)
|
|
11
|
-
reset ()
|
|
9
|
+
setSenderSeqNum: (value: number) => Promise<void>
|
|
10
|
+
setTargetSeqNum: (value: number) => Promise<void>
|
|
11
|
+
reset: () => Promise<void>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IJsFixConfig } from '../../config'
|
|
1
|
+
import { IJsFixConfig, IJsFixLogger } from '../../config'
|
|
2
2
|
import { FixAcceptor } from '../fix-acceptor'
|
|
3
3
|
import { TcpAcceptor } from './tcp-acceptor'
|
|
4
4
|
import { MsgTransport } from '../factory'
|
|
@@ -6,14 +6,23 @@ import { inject, injectable } from 'tsyringe'
|
|
|
6
6
|
import { FixSession } from '../session/fix-session'
|
|
7
7
|
import { DITokens } from '../../runtime/di-tokens'
|
|
8
8
|
import { FixEntity } from '../fix-entity'
|
|
9
|
+
import { ISessionRegistry, SessionRegistry } from '../session/session-registry'
|
|
9
10
|
|
|
10
11
|
@injectable()
|
|
11
12
|
export class TcpAcceptorListener extends FixEntity {
|
|
12
13
|
private acceptor: FixAcceptor | null = null
|
|
13
14
|
private resolveStart: ((value: any) => void) | null = null
|
|
15
|
+
/**
|
|
16
|
+
* One registry per listener, so a counterparty reconnecting onto a fresh socket
|
|
17
|
+
* replaces its previous session instead of running alongside it (issue #153).
|
|
18
|
+
* An application may supply its own via config.sessionRegistry.
|
|
19
|
+
*/
|
|
20
|
+
public readonly sessionRegistry: ISessionRegistry
|
|
14
21
|
|
|
15
22
|
constructor (@inject(DITokens.IJsFixConfig) public readonly config: IJsFixConfig) {
|
|
16
23
|
super(config)
|
|
24
|
+
this.sessionRegistry = config.sessionRegistry ?? new SessionRegistry(config.logFactory)
|
|
25
|
+
config.sessionRegistry = this.sessionRegistry
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
async start (): Promise<any> {
|
|
@@ -29,24 +38,53 @@ export class TcpAcceptorListener extends FixEntity {
|
|
|
29
38
|
this.acceptor = acceptor
|
|
30
39
|
|
|
31
40
|
acceptor.on('transport', (t: MsgTransport) => {
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
// resolve from the transport's own scope, not the listener's container, so
|
|
42
|
+
// this session gets the per-connection description, buffers and message
|
|
43
|
+
// factory created by TcpAcceptor. Falls back to the listener container for
|
|
44
|
+
// transports created outside the acceptor (tests, custom entities).
|
|
45
|
+
const scope = t.config?.sessionContainer ?? sessionContainer
|
|
46
|
+
const description = t.config?.description
|
|
47
|
+
logger.info(`transport ${t.id}: creating session via DI token ${DITokens.FixSession} ` +
|
|
48
|
+
`[${description?.SenderCompId ?? '?'} -> ${description?.TargetCompID ?? '?'}]`)
|
|
49
|
+
const acceptorSession = scope.resolve<FixSession>(DITokens.FixSession)
|
|
34
50
|
this.emit('session', acceptorSession, t)
|
|
51
|
+
// a wildcard acceptor has not claimed its SessionId yet - report again once
|
|
52
|
+
// the session ends, by which time the registry line is meaningful
|
|
53
|
+
this.census(logger)
|
|
35
54
|
// Run the session but do NOT close the listener when it ends.
|
|
36
55
|
// The acceptor keeps listening for new connections (reconnects).
|
|
37
56
|
// This matches the C# TcpAcceptorListener accept-loop pattern.
|
|
38
57
|
acceptorSession.run(t).then(() => {
|
|
39
|
-
logger.info(
|
|
58
|
+
logger.info(`transport ${t.id}: session ended normally, acceptor continues listening`)
|
|
59
|
+
this.census(logger)
|
|
40
60
|
}).catch((e: Error) => {
|
|
41
|
-
logger.warning(`session ended with error: ${e.message}, acceptor continues listening`)
|
|
61
|
+
logger.warning(`transport ${t.id}: session ended with error: ${e.message}, acceptor continues listening`)
|
|
62
|
+
this.census(logger)
|
|
42
63
|
})
|
|
43
64
|
})
|
|
44
65
|
|
|
66
|
+
acceptor.on('harvested', (tid: number, reason: string) => {
|
|
67
|
+
logger.info(`transport ${tid} harvested (${reason})`)
|
|
68
|
+
this.census(logger)
|
|
69
|
+
})
|
|
70
|
+
|
|
45
71
|
this.resolveStart = resolve
|
|
46
72
|
acceptor.listen()
|
|
47
73
|
})
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
/**
|
|
77
|
+
* What is actually connected, and which SessionIds are claimed. Emitted whenever
|
|
78
|
+
* the population changes so "how many clients do I have, and who are they" is
|
|
79
|
+
* answerable from the server log alone.
|
|
80
|
+
*/
|
|
81
|
+
private census (logger: IJsFixLogger): void {
|
|
82
|
+
const acceptor = this.acceptor as TcpAcceptor | null
|
|
83
|
+
const transports = acceptor?.liveTransports() ?? []
|
|
84
|
+
logger.info(`acceptor census: transports=${transports.length} [${transports.join(', ')}] ` +
|
|
85
|
+
`sessions=${this.sessionRegistry.count} [${this.sessionRegistry.keys().join(', ')}]`)
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
/**
|
|
51
89
|
* Explicitly stop the acceptor listener. Call this when the application
|
|
52
90
|
* is shutting down (e.g., timeout, SIGINT). Without this call, the
|