jspurefix 5.9.2 → 5.10.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 +35 -0
- package/dist/buffer/ascii/ascii-encoder.d.ts +1 -0
- package/dist/buffer/ascii/ascii-encoder.js +15 -0
- package/dist/buffer/ascii/ascii-encoder.js.map +1 -1
- package/dist/buffer/msg-encoder.d.ts +2 -0
- package/dist/buffer/msg-encoder.js +1 -0
- package/dist/buffer/msg-encoder.js.map +1 -1
- package/dist/runtime/session-container.d.ts +3 -0
- package/dist/runtime/session-container.js +8 -0
- package/dist/runtime/session-container.js.map +1 -1
- package/dist/runtime/session-launcher.d.ts +2 -1
- package/dist/runtime/session-launcher.js +4 -1
- package/dist/runtime/session-launcher.js.map +1 -1
- package/dist/transport/ascii/ascii-msg-transmitter.d.ts +1 -0
- package/dist/transport/ascii/ascii-msg-transmitter.js +15 -0
- package/dist/transport/ascii/ascii-msg-transmitter.js.map +1 -1
- package/dist/transport/ascii/ascii-session-msg-factory.js +1 -7
- package/dist/transport/ascii/ascii-session-msg-factory.js.map +1 -1
- package/dist/transport/fixml/fixml-session-msg-factory.js +1 -6
- package/dist/transport/fixml/fixml-session-msg-factory.js.map +1 -1
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/msg-payload.d.ts +3 -1
- package/dist/transport/msg-payload.js +2 -1
- package/dist/transport/msg-payload.js.map +1 -1
- package/dist/transport/msg-transmitter.d.ts +4 -1
- package/dist/transport/msg-transmitter.js +18 -2
- package/dist/transport/msg-transmitter.js.map +1 -1
- package/dist/transport/send-callback.d.ts +7 -0
- package/dist/transport/send-callback.js +3 -0
- package/dist/transport/send-callback.js.map +1 -0
- package/dist/transport/session/fix-session.d.ts +2 -1
- package/dist/transport/session/fix-session.js +12 -3
- package/dist/transport/session/fix-session.js.map +1 -1
- package/dist/transport/session/session-description.d.ts +2 -0
- package/dist/transport/session/session-description.js.map +1 -1
- package/docs/custom-logon.md +225 -0
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +1 -1
- package/src/buffer/ascii/ascii-encoder.ts +20 -0
- package/src/buffer/msg-encoder.ts +12 -0
- package/src/runtime/session-container.ts +20 -0
- package/src/runtime/session-launcher.ts +19 -2
- package/src/transport/ascii/ascii-msg-transmitter.ts +20 -0
- package/src/transport/ascii/ascii-session-msg-factory.ts +6 -1
- package/src/transport/fixml/fixml-session-msg-factory.ts +3 -1
- package/src/transport/index.ts +5 -0
- package/src/transport/msg-payload.ts +5 -1
- package/src/transport/msg-transmitter.ts +31 -2
- package/src/transport/send-callback.ts +33 -0
- package/src/transport/session/fix-session.ts +22 -3
- package/src/transport/session/session-description.ts +14 -0
|
@@ -20,7 +20,23 @@ import { FixmlEncoder, FiXmlParser } from '../buffer/fixml'
|
|
|
20
20
|
import { FixEntity } from '../transport/fix-entity'
|
|
21
21
|
import { IHttpAdapter } from '../transport/http/http-adapter'
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Supplies the session message factory for a description. Return null to accept the
|
|
25
|
+
* stock factory for the protocol.
|
|
26
|
+
*/
|
|
27
|
+
export type SessionMsgFactoryProvider = (description: ISessionDescription) => ISessionMsgFactory | null
|
|
28
|
+
|
|
23
29
|
export class SessionContainer {
|
|
30
|
+
/**
|
|
31
|
+
* @param msgFactoryProvider an application that needs its own session messages -
|
|
32
|
+
* typically a Logon carrying tags the standard message does not - can pass one
|
|
33
|
+
* here rather than subclassing to override makeSessionFactory:
|
|
34
|
+
*
|
|
35
|
+
* new SessionContainer(d => new MyFactory(d))
|
|
36
|
+
*/
|
|
37
|
+
constructor (public readonly msgFactoryProvider: SessionMsgFactoryProvider | null = null) {
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
public reset (): void {
|
|
25
41
|
container.reset()
|
|
26
42
|
}
|
|
@@ -45,6 +61,10 @@ export class SessionContainer {
|
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
protected makeSessionFactory (description: ISessionDescription): ISessionMsgFactory {
|
|
64
|
+
const supplied = this.msgFactoryProvider?.(description)
|
|
65
|
+
if (supplied) {
|
|
66
|
+
return supplied
|
|
67
|
+
}
|
|
48
68
|
const fixml = !this.isAscii(description)
|
|
49
69
|
return fixml
|
|
50
70
|
? new FixmlSessionMsgFactory(description)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from 'path'
|
|
2
2
|
import { IJsFixConfig, IJsFixLogger, JsFixLoggerFactory, JsFixWinstonLogFactory, WinstonLogger } from '../config'
|
|
3
|
-
import { FixEntity, FixSession, ISessionDescription } from '../transport'
|
|
3
|
+
import { FixEntity, FixSession, ISessionDescription, ISessionMsgFactory } from '../transport'
|
|
4
4
|
import { DependencyContainer } from 'tsyringe'
|
|
5
5
|
import { EngineFactory } from './engine-factory'
|
|
6
6
|
import { SessionContainer } from './session-container'
|
|
@@ -24,7 +24,24 @@ export abstract class SessionLauncher {
|
|
|
24
24
|
this.acceptorConfig = acceptorConfig ? this.loadConfig(acceptorConfig) : null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// the arrow defers to makeSessionMsgFactory at resolve time, so a subclass need
|
|
28
|
+
// only override that one method - no SessionContainer subclass required
|
|
29
|
+
protected sessionContainer: SessionContainer =
|
|
30
|
+
new SessionContainer((d: ISessionDescription) => this.makeSessionMsgFactory(d))
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Override to supply this application's own session message factory - the place to
|
|
34
|
+
* put a Logon carrying tags the standard message does not, or a header a
|
|
35
|
+
* counterparty stamps differently. Return null (the default) for the stock
|
|
36
|
+
* factory.
|
|
37
|
+
*
|
|
38
|
+
* For a Logon that only needs extra fields, prefer the "Logon" block in the session
|
|
39
|
+
* description - it needs no code at all. Reach for a factory when the values are
|
|
40
|
+
* computed at run time (a signature, a nonce, a token fetched at start up).
|
|
41
|
+
*/
|
|
42
|
+
protected makeSessionMsgFactory (_description: ISessionDescription): ISessionMsgFactory | null {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
private async empty (): Promise<any> {
|
|
30
47
|
return await new Promise((resolve, reject) => {
|
|
@@ -28,6 +28,26 @@ export class AsciiMsgTransmitter extends MsgTransmitter {
|
|
|
28
28
|
const components = config.definitions.component
|
|
29
29
|
this.header = components.get('StandardHeader')
|
|
30
30
|
this.trailer = components.get('StandardTrailer')
|
|
31
|
+
this.reportUnknownFields(config)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A field the dictionary does not declare on the message being encoded has no tag,
|
|
36
|
+
* so it cannot be written and is dropped. Say so once per offending field - the
|
|
37
|
+
* alternative, and what this library did for years, is that a bespoke Logon tag
|
|
38
|
+
* simply never appears on the wire and nothing anywhere explains why.
|
|
39
|
+
*/
|
|
40
|
+
private reportUnknownFields (config: IJsFixConfig): void {
|
|
41
|
+
const name = config.description.application?.name ?? 'na'
|
|
42
|
+
const logger = config.logFactory.logger(`${name}:encoder`)
|
|
43
|
+
const reported = new Set<string>()
|
|
44
|
+
this.encoder.onUnknownField = (fieldName: string, setName: string) => {
|
|
45
|
+
const key = `${setName}.${fieldName}`
|
|
46
|
+
if (reported.has(key)) return
|
|
47
|
+
reported.add(key)
|
|
48
|
+
logger.warning(`field '${fieldName}' is not declared on ${setName} in this dictionary - it will not be sent`)
|
|
49
|
+
this.emit('unknown-field', fieldName, setName)
|
|
50
|
+
}
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
private checksum (): number {
|
|
@@ -23,7 +23,12 @@ export class AsciiSessionMsgFactory extends ASessionMsgFactory {
|
|
|
23
23
|
Password: description.Password,
|
|
24
24
|
HeartBtInt: description.HeartBtInt,
|
|
25
25
|
ResetSeqNumFlag: description.ResetSeqNumFlag,
|
|
26
|
-
EncryptMethod: EncryptMethod.None
|
|
26
|
+
EncryptMethod: EncryptMethod.None,
|
|
27
|
+
// a counterparty demanding a tag the standard Logon does not carry needs no
|
|
28
|
+
// code at all - name it under "Logon" in the session description. Spread last
|
|
29
|
+
// so an application can also override what the engine derived, or null out a
|
|
30
|
+
// field it does not want sent. See issue #93.
|
|
31
|
+
...description.Logon
|
|
27
32
|
} as ILogon
|
|
28
33
|
return this.mutate(o, MsgType.Logon)
|
|
29
34
|
}
|
|
@@ -42,7 +42,9 @@ export class FixmlSessionMsgFactory extends ASessionMsgFactory {
|
|
|
42
42
|
Username: description.Username,
|
|
43
43
|
Password: description.Password,
|
|
44
44
|
UserRequestID: userRequestId,
|
|
45
|
-
UserRequestType: UserRequestType.LogOnUser
|
|
45
|
+
UserRequestType: UserRequestType.LogOnUser,
|
|
46
|
+
// same escape hatch as the ascii factory - see ISessionDescription.Logon
|
|
47
|
+
...description.Logon
|
|
46
48
|
} as IUserRequest
|
|
47
49
|
return this.mutate(o, MsgType.Logon)
|
|
48
50
|
} else {
|
package/src/transport/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ export * from './fix-acceptor'
|
|
|
4
4
|
export * from './fix-initiator'
|
|
5
5
|
export * from './msg-transmitter'
|
|
6
6
|
export * from './msg-payload'
|
|
7
|
+
export * from './send-callback'
|
|
7
8
|
export * from './tick-action'
|
|
8
9
|
export * from './msg-application'
|
|
9
10
|
export * from './ascii/ascii-session'
|
|
11
|
+
// the factory an application extends to customise its Logon or header - it belongs
|
|
12
|
+
// at the root alongside ASessionMsgFactory, not behind a dist/ path
|
|
13
|
+
export * from './ascii/ascii-session-msg-factory'
|
|
14
|
+
export * from './fixml/fixml-session-msg-factory'
|
|
10
15
|
export * from './session'
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ILooseObject } from '../collections/collection'
|
|
2
|
+
import { SendCallback } from './send-callback'
|
|
2
3
|
|
|
3
4
|
export class MsgPayload {
|
|
4
5
|
public encoded: Buffer
|
|
5
|
-
constructor (public readonly msgType: string,
|
|
6
|
+
constructor (public readonly msgType: string,
|
|
7
|
+
public readonly obj: ILooseObject,
|
|
8
|
+
/** invoked once this payload has been encoded, or has failed to encode */
|
|
9
|
+
public readonly callback: SendCallback | null = null) {
|
|
6
10
|
}
|
|
7
11
|
}
|
|
@@ -4,6 +4,7 @@ import { Transform } from 'stream'
|
|
|
4
4
|
import { MsgPayload } from './msg-payload'
|
|
5
5
|
import { ILooseObject } from '../collections/collection'
|
|
6
6
|
import { ISessionDescription } from './session/session-description'
|
|
7
|
+
import { SendCallback } from './send-callback'
|
|
7
8
|
import * as events from 'events'
|
|
8
9
|
|
|
9
10
|
export abstract class MsgTransmitter extends events.EventEmitter {
|
|
@@ -24,8 +25,23 @@ export abstract class MsgTransmitter extends events.EventEmitter {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// messages at front, byte stream at back
|
|
27
|
-
public send (msgType: string, obj: ILooseObject): void {
|
|
28
|
-
this.encodeStream.write(new MsgPayload(msgType, obj))
|
|
28
|
+
public send (msgType: string, obj: ILooseObject, callback: SendCallback | null = null): void {
|
|
29
|
+
this.encodeStream.write(new MsgPayload(msgType, obj, callback))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Report a send outcome to whoever asked for one, without letting their code break
|
|
34
|
+
* the encode stream - a throwing callback would otherwise surface as a transform
|
|
35
|
+
* failure and be indistinguishable from an encoding fault.
|
|
36
|
+
*/
|
|
37
|
+
protected settle (payload: MsgPayload, error: Error | null, header: ILooseObject | null): void {
|
|
38
|
+
const callback = payload.callback
|
|
39
|
+
if (!callback) return
|
|
40
|
+
try {
|
|
41
|
+
callback(error, { msgType: payload.msgType, header, encoded: payload.encoded ?? null })
|
|
42
|
+
} catch (e) {
|
|
43
|
+
this.emit('error', e)
|
|
44
|
+
}
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
public abstract encodeMessage (msgType: string, obj: ILooseObject): (ILooseObject | null)
|
|
@@ -46,8 +62,21 @@ export abstract class MsgTransmitter extends events.EventEmitter {
|
|
|
46
62
|
this.push(payload.encoded)
|
|
47
63
|
const encodedTxt = transmitter.buffer.toString()
|
|
48
64
|
transmitter.emit('encoded', msgType, encodedTxt, state)
|
|
65
|
+
// encodeMessage returns null when it could not build the message - an
|
|
66
|
+
// unknown msgType, or a factory that produced no header. It reports that
|
|
67
|
+
// on the error channel and carries on, so without this the callback would
|
|
68
|
+
// announce success for a message that never formed.
|
|
69
|
+
state
|
|
70
|
+
? transmitter.settle(payload, null, state)
|
|
71
|
+
: transmitter.settle(payload, new Error(`could not encode ${msgType}`), null)
|
|
72
|
+
// note: the second argument to a Transform callback is *pushed to the
|
|
73
|
+
// readable side*, and this readable side is piped to the socket - so a
|
|
74
|
+
// result cannot be returned that way without corrupting the byte stream.
|
|
75
|
+
// Hence the per-payload callback above.
|
|
49
76
|
done()
|
|
50
77
|
} catch (e) {
|
|
78
|
+
transmitter.settle(payload, e as Error, null)
|
|
79
|
+
// still fail the stream, so the session's error handling is unchanged
|
|
51
80
|
done(e)
|
|
52
81
|
}
|
|
53
82
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ILooseObject } from '../collections/collection'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a send produced. `header` is the StandardHeader the engine stamped on the
|
|
5
|
+
* message - the sequence number it went out under, the sending time, the comp ids -
|
|
6
|
+
* which is the piece applications most often need to correlate an outbound message
|
|
7
|
+
* with an acknowledgement, a database row, or a resend.
|
|
8
|
+
*/
|
|
9
|
+
export interface SendResult {
|
|
10
|
+
msgType: string
|
|
11
|
+
/** the stamped StandardHeader, or null if the message never got that far */
|
|
12
|
+
header: ILooseObject | null
|
|
13
|
+
/** the bytes written to the transport */
|
|
14
|
+
encoded: Buffer | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Called once per `send`, after the message has been encoded and pushed to the
|
|
19
|
+
* transport, or as soon as encoding fails.
|
|
20
|
+
*
|
|
21
|
+
* `send` is otherwise fire and forget: it writes into a stream and returns, so
|
|
22
|
+
* nothing tells the caller which sequence number their message went out under, and a
|
|
23
|
+
* failure surfaces on the session's error channel with no way to tell which call
|
|
24
|
+
* caused it. See https://github.com/TimelordUK/jspurefix/issues/86
|
|
25
|
+
*
|
|
26
|
+
* Note this fires when the message has been *encoded and handed to the transport*,
|
|
27
|
+
* not when the socket has flushed it - the FIX session layer has no acknowledgement
|
|
28
|
+
* of the latter either.
|
|
29
|
+
*
|
|
30
|
+
* A callback that throws is reported on the transmitter's error channel rather than
|
|
31
|
+
* being allowed to tear down the encode stream.
|
|
32
|
+
*/
|
|
33
|
+
export type SendCallback = (error: Error | null, result: SendResult) => void
|
|
@@ -4,6 +4,7 @@ import { FixSessionState } from './fix-session-state'
|
|
|
4
4
|
import { MsgTransport } from '../factory'
|
|
5
5
|
import { MsgTag } from '../../types'
|
|
6
6
|
import { ILooseObject } from '../../collections/collection'
|
|
7
|
+
import { SendCallback } from '../send-callback'
|
|
7
8
|
|
|
8
9
|
import * as events from 'events'
|
|
9
10
|
import { SessionState } from './session-state'
|
|
@@ -401,17 +402,35 @@ export abstract class FixSession extends events.EventEmitter {
|
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
|
|
404
|
-
|
|
405
|
+
/**
|
|
406
|
+
* @param callback optional, invoked once the message has been encoded and handed to
|
|
407
|
+
* the transport, or as soon as that fails. Without it a send is fire and forget:
|
|
408
|
+
* nothing tells the caller which sequence number the message went out under, and a
|
|
409
|
+
* message dropped because the session is down, or refused by the encoder, goes
|
|
410
|
+
* unanswered. See https://github.com/TimelordUK/jspurefix/issues/86
|
|
411
|
+
*/
|
|
412
|
+
protected send (msgType: string, obj: ILooseObject, callback: SendCallback | null = null): void {
|
|
405
413
|
const state = this.sessionState.state
|
|
406
414
|
switch (state) {
|
|
407
415
|
case SessionState.Stopped: {
|
|
408
|
-
|
|
416
|
+
const msg = `can't send in state ${this.stateString()}`
|
|
417
|
+
this.sessionLogger.warning(msg)
|
|
418
|
+
// the message is dropped either way - but a caller that asked to be told is
|
|
419
|
+
// now told, instead of watching for a log line
|
|
420
|
+
callback?.(new Error(msg), { msgType, header: null, encoded: null })
|
|
409
421
|
break
|
|
410
422
|
}
|
|
411
423
|
|
|
412
424
|
default: {
|
|
413
425
|
this.sessionState.LastSentAt = new Date()
|
|
414
|
-
this.transport?.transmitter
|
|
426
|
+
const transmitter = this.transport?.transmitter
|
|
427
|
+
if (!transmitter) {
|
|
428
|
+
const msg = `no transport to send ${msgType} on`
|
|
429
|
+
this.sessionLogger.warning(msg)
|
|
430
|
+
callback?.(new Error(msg), { msgType, header: null, encoded: null })
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
transmitter.send(msgType, obj, callback)
|
|
415
434
|
break
|
|
416
435
|
}
|
|
417
436
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IMsgApplication } from '../msg-application'
|
|
2
2
|
import { StoreConfig } from '../../store/store-config'
|
|
3
|
+
import { ILooseObject } from '../../collections/collection'
|
|
3
4
|
|
|
4
5
|
export interface IDynamicSessionParams {
|
|
5
6
|
readonly Name: string
|
|
@@ -15,6 +16,19 @@ export interface IDynamicSessionParams {
|
|
|
15
16
|
|
|
16
17
|
export interface ISessionDescription extends IDynamicSessionParams {
|
|
17
18
|
readonly application?: IMsgApplication
|
|
19
|
+
/**
|
|
20
|
+
* Extra fields to place on the outbound Logon, merged over the ones the engine
|
|
21
|
+
* derives from this description. Counterparties routinely demand a tag the
|
|
22
|
+
* standard Logon does not carry - an account, a default application version, a
|
|
23
|
+
* free text token - and this is the no-code way to send it:
|
|
24
|
+
*
|
|
25
|
+
* "Logon": { "Account": "TVKD_001", "DefaultApplVerID": "9" }
|
|
26
|
+
*
|
|
27
|
+
* A field named here still has to exist on Logon in the dictionary the session
|
|
28
|
+
* loads, otherwise the encoder has no tag to write it to. Set a field to null to
|
|
29
|
+
* suppress one the engine would otherwise send. See issues #93, #39, #96.
|
|
30
|
+
*/
|
|
31
|
+
readonly Logon?: ILooseObject
|
|
18
32
|
HeartBtInt: number
|
|
19
33
|
LastSentSeqNum?: number
|
|
20
34
|
readonly LastReceivedSeqNum?: number
|