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
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ConnectionOptions, TLSSocket, TlsOptions } from 'tls';
|
|
2
|
+
export declare function describeConnectionOptions(options: ConnectionOptions): string;
|
|
3
|
+
export declare function describeServerOptions(options: TlsOptions): string;
|
|
4
|
+
export declare function describeNegotiated(socket: TLSSocket): string;
|
|
5
|
+
export declare function describePeerCertificate(socket: TLSSocket): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeConnectionOptions = describeConnectionOptions;
|
|
4
|
+
exports.describeServerOptions = describeServerOptions;
|
|
5
|
+
exports.describeNegotiated = describeNegotiated;
|
|
6
|
+
exports.describePeerCertificate = describePeerCertificate;
|
|
7
|
+
function describeCommonOptions(options) {
|
|
8
|
+
var _a, _b;
|
|
9
|
+
const parts = [];
|
|
10
|
+
parts.push(`key ${options.key ? 'supplied' : 'none'}`);
|
|
11
|
+
parts.push(`cert ${options.cert ? 'supplied' : 'none'}`);
|
|
12
|
+
const ca = options.ca;
|
|
13
|
+
const caCount = Array.isArray(ca) ? ca.length : (ca ? 1 : 0);
|
|
14
|
+
parts.push(`ca ${caCount}`);
|
|
15
|
+
parts.push(`rejectUnauthorized ${String((_a = options.rejectUnauthorized) !== null && _a !== void 0 ? _a : '(default true)')}`);
|
|
16
|
+
parts.push(`requestCert ${String((_b = options.requestCert) !== null && _b !== void 0 ? _b : '(default false)')}`);
|
|
17
|
+
if (options.minVersion)
|
|
18
|
+
parts.push(`minVersion ${options.minVersion}`);
|
|
19
|
+
if (options.maxVersion)
|
|
20
|
+
parts.push(`maxVersion ${options.maxVersion}`);
|
|
21
|
+
return parts;
|
|
22
|
+
}
|
|
23
|
+
function describeConnectionOptions(options) {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
const parts = [`host ${(_a = options.host) !== null && _a !== void 0 ? _a : '?'}`, `port ${(_b = options.port) !== null && _b !== void 0 ? _b : '?'}`];
|
|
26
|
+
if (options.servername)
|
|
27
|
+
parts.push(`servername ${options.servername}`);
|
|
28
|
+
return parts.concat(describeCommonOptions(options)).join(', ');
|
|
29
|
+
}
|
|
30
|
+
function describeServerOptions(options) {
|
|
31
|
+
return describeCommonOptions(options).join(', ');
|
|
32
|
+
}
|
|
33
|
+
function describeNegotiated(socket) {
|
|
34
|
+
var _a;
|
|
35
|
+
const parts = [];
|
|
36
|
+
parts.push(`protocol ${(_a = socket.getProtocol()) !== null && _a !== void 0 ? _a : 'none'}`);
|
|
37
|
+
const cipher = socket.getCipher();
|
|
38
|
+
parts.push(`cipher ${cipher ? `${cipher.name} (${cipher.version})` : 'none'}`);
|
|
39
|
+
parts.push(`authorized ${socket.authorized}`);
|
|
40
|
+
if (!socket.authorized && socket.authorizationError) {
|
|
41
|
+
parts.push(`authorizationError ${String(socket.authorizationError)}`);
|
|
42
|
+
}
|
|
43
|
+
return parts.join(', ');
|
|
44
|
+
}
|
|
45
|
+
function describePeerCertificate(socket) {
|
|
46
|
+
var _a, _b, _c, _d;
|
|
47
|
+
const cert = socket.getPeerCertificate(false);
|
|
48
|
+
if (!cert || Object.keys(cert).length === 0) {
|
|
49
|
+
return 'peer presented no certificate';
|
|
50
|
+
}
|
|
51
|
+
const subject = cert.subject ? ((_a = cert.subject.CN) !== null && _a !== void 0 ? _a : JSON.stringify(cert.subject)) : '?';
|
|
52
|
+
const issuer = cert.issuer ? ((_b = cert.issuer.CN) !== null && _b !== void 0 ? _b : JSON.stringify(cert.issuer)) : '?';
|
|
53
|
+
const parts = [
|
|
54
|
+
`subject ${subject}`,
|
|
55
|
+
`issuer ${issuer}`,
|
|
56
|
+
`valid ${(_c = cert.valid_from) !== null && _c !== void 0 ? _c : '?'} to ${(_d = cert.valid_to) !== null && _d !== void 0 ? _d : '?'}`
|
|
57
|
+
];
|
|
58
|
+
if (cert.subjectaltname)
|
|
59
|
+
parts.push(`san ${cert.subjectaltname}`);
|
|
60
|
+
if (cert.fingerprint256)
|
|
61
|
+
parts.push(`sha256 ${cert.fingerprint256}`);
|
|
62
|
+
return parts.join(', ');
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=tls-diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls-diagnostics.js","sourceRoot":"","sources":["../../../src/transport/tcp/tls-diagnostics.ts"],"names":[],"mappings":"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","sourcesContent":["import { ConnectionOptions, TLSSocket, TlsOptions } from 'tls'\n\n/*\n tls failures are routinely reported as \"it just times out\" - see issues #94 and #151. the\n session either connects or it does not, and nothing in the log says which options were\n actually applied or what the peer presented. enableTrace exists but is node's raw handshake\n trace: all or nothing, written to stderr rather than through IJsFixLogger.\n\n these helpers render the small number of facts needed to diagnose a handshake, and are\n deliberately conservative about what they include - never key material, never a full\n certificate, only the identifying fields of one.\n */\n\nfunction describeCommonOptions (options: ConnectionOptions | TlsOptions): string[] {\n const parts: string[] = []\n // presence only - the values are private keys and certificate bodies.\n parts.push(`key ${options.key ? 'supplied' : 'none'}`)\n parts.push(`cert ${options.cert ? 'supplied' : 'none'}`)\n const ca = options.ca\n const caCount = Array.isArray(ca) ? ca.length : (ca ? 1 : 0)\n parts.push(`ca ${caCount}`)\n // the whole point of #94/#151 - make it unambiguous which value won.\n parts.push(`rejectUnauthorized ${String(options.rejectUnauthorized ?? '(default true)')}`)\n parts.push(`requestCert ${String(options.requestCert ?? '(default false)')}`)\n if (options.minVersion) parts.push(`minVersion ${options.minVersion}`)\n if (options.maxVersion) parts.push(`maxVersion ${options.maxVersion}`)\n return parts\n}\n\nexport function describeConnectionOptions (options: ConnectionOptions): string {\n const parts: string[] = [`host ${options.host ?? '?'}`, `port ${options.port ?? '?'}`]\n if (options.servername) parts.push(`servername ${options.servername}`)\n return parts.concat(describeCommonOptions(options)).join(', ')\n}\n\nexport function describeServerOptions (options: TlsOptions): string {\n return describeCommonOptions(options).join(', ')\n}\n\n/*\n what the handshake actually negotiated. the maintainer's suggestion in #94 was to reproduce\n the connection in powershell to see the negotiated suite - this puts the same facts in our\n own log instead.\n */\nexport function describeNegotiated (socket: TLSSocket): string {\n const parts: string[] = []\n parts.push(`protocol ${socket.getProtocol() ?? 'none'}`)\n const cipher = socket.getCipher()\n parts.push(`cipher ${cipher ? `${cipher.name} (${cipher.version})` : 'none'}`)\n parts.push(`authorized ${socket.authorized}`)\n if (!socket.authorized && socket.authorizationError) {\n parts.push(`authorizationError ${String(socket.authorizationError)}`)\n }\n return parts.join(', ')\n}\n\n/*\n identifying fields of the peer certificate only. getPeerCertificate() returns the full DER\n in .raw plus the public key - none of that belongs in a log, so pick fields explicitly.\n */\nexport function describePeerCertificate (socket: TLSSocket): string {\n const cert = socket.getPeerCertificate(false)\n if (!cert || Object.keys(cert).length === 0) {\n return 'peer presented no certificate'\n }\n const subject = cert.subject ? (cert.subject.CN ?? JSON.stringify(cert.subject)) : '?'\n const issuer = cert.issuer ? (cert.issuer.CN ?? JSON.stringify(cert.issuer)) : '?'\n const parts: string[] = [\n `subject ${subject}`,\n `issuer ${issuer}`,\n `valid ${cert.valid_from ?? '?'} to ${cert.valid_to ?? '?'}`\n ]\n if (cert.subjectaltname) parts.push(`san ${cert.subjectaltname}`)\n if (cert.fingerprint256) parts.push(`sha256 ${cert.fingerprint256}`)\n return parts.join(', ')\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import { ConnectionOptions, TlsOptions } from 'tls';
|
|
|
2
2
|
import { ITlsOptions } from './tls-options';
|
|
3
3
|
import { ITcpTransportDescription } from './tcp-transport-description';
|
|
4
4
|
export declare class TlsOptionsFactory {
|
|
5
|
+
static asBoolean(value: any): any;
|
|
5
6
|
static read(filePath: string): string;
|
|
6
7
|
static getTlsOptions(tls: ITlsOptions): TlsOptions | null;
|
|
7
8
|
static getTlsConnectionOptions(tcp: ITcpTransportDescription): ConnectionOptions | null;
|
|
@@ -4,6 +4,16 @@ exports.TlsOptionsFactory = void 0;
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
class TlsOptionsFactory {
|
|
7
|
+
static asBoolean(value) {
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
const lowered = value.toLowerCase();
|
|
10
|
+
if (lowered === 'true')
|
|
11
|
+
return true;
|
|
12
|
+
if (lowered === 'false')
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
7
17
|
static read(filePath) {
|
|
8
18
|
const root = path.join(__dirname, '../../../');
|
|
9
19
|
const fullPath = path.join(root, filePath);
|
|
@@ -15,8 +25,8 @@ class TlsOptionsFactory {
|
|
|
15
25
|
let tlsOptions = null;
|
|
16
26
|
if (tls) {
|
|
17
27
|
tlsOptions = {
|
|
18
|
-
requestCert: tls.requestCert,
|
|
19
|
-
rejectUnauthorized: tls.rejectUnauthorized
|
|
28
|
+
requestCert: TlsOptionsFactory.asBoolean(tls.requestCert),
|
|
29
|
+
rejectUnauthorized: TlsOptionsFactory.asBoolean(tls.rejectUnauthorized)
|
|
20
30
|
};
|
|
21
31
|
if (tls.key) {
|
|
22
32
|
tlsOptions.key = TlsOptionsFactory.read(tls.key);
|
|
@@ -53,8 +63,20 @@ class TlsOptionsFactory {
|
|
|
53
63
|
if (tcp.tls.sessionTimeout) {
|
|
54
64
|
connectionOptions.sessionTimeout = tcp.tls.sessionTimeout;
|
|
55
65
|
}
|
|
66
|
+
if (tls.requestCert !== undefined) {
|
|
67
|
+
connectionOptions.requestCert = TlsOptionsFactory.asBoolean(tls.requestCert);
|
|
68
|
+
}
|
|
69
|
+
if (tls.rejectUnauthorized !== undefined) {
|
|
70
|
+
connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(tls.rejectUnauthorized);
|
|
71
|
+
}
|
|
56
72
|
if (tls.nodeTlsConnectionOptions) {
|
|
57
73
|
connectionOptions = Object.assign(Object.assign({}, connectionOptions), tls.nodeTlsConnectionOptions);
|
|
74
|
+
if (connectionOptions.rejectUnauthorized !== undefined) {
|
|
75
|
+
connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(connectionOptions.rejectUnauthorized);
|
|
76
|
+
}
|
|
77
|
+
if (connectionOptions.requestCert !== undefined) {
|
|
78
|
+
connectionOptions.requestCert = TlsOptionsFactory.asBoolean(connectionOptions.requestCert);
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
82
|
return connectionOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tls-options-factory.js","sourceRoot":"","sources":["../../../src/transport/tcp/tls-options-factory.ts"],"names":[],"mappings":";;;AAGA,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAC5B,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAExB,MAAa,iBAAiB;
|
|
1
|
+
{"version":3,"file":"tls-options-factory.js","sourceRoot":"","sources":["../../../src/transport/tcp/tls-options-factory.ts"],"names":[],"mappings":";;;AAGA,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAC5B,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAExB,MAAa,iBAAiB;IAQ5B,MAAM,CAAC,SAAS,CAAE,KAAU;QAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;YACnC,IAAI,OAAO,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAA;YACnC,IAAI,OAAO,KAAK,OAAO;gBAAE,OAAO,KAAK,CAAA;QACvC,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,CAAC,IAAI,CAAE,QAAgB;QAC3B,MAAM,IAAI,GAAW,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC1C,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAC7B;YACE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;SAC5B,CAAC,CAAA;IACN,CAAC;IAED,MAAM,CAAC,aAAa,CAAE,GAAgB;QACpC,IAAI,UAAU,GAAsB,IAAI,CAAA;QACxC,IAAI,GAAG,EAAE,CAAC;YACR,UAAU,GAAG;gBACX,WAAW,EAAE,iBAAiB,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC;gBACzD,kBAAkB,EAAE,iBAAiB,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC;aAC1D,CAAA;YAEf,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;gBACZ,UAAU,CAAC,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBAChD,UAAU,CAAC,IAAI,GAAG,CAAA,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,EAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;YAC7E,CAAC;YAED,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,UAAU,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAC5D,CAAC;YAED,IAAI,GAAG,CAAC,oBAAoB,EAAE,CAAC;gBAC7B,UAAU,mCACL,UAAU,GACV,GAAG,CAAC,oBAAoB,CAC5B,CAAA;YACH,CAAC;QACH,CAAC;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,MAAM,CAAC,uBAAuB,CAAE,GAA6B;;QAC3D,IAAI,iBAAiB,GAA6B,IAAI,CAAA;QACtD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAA;QACnB,IAAI,GAAG,EAAE,CAAC;YACR,iBAAiB,GAAG;gBAClB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI,EAAE,GAAG,CAAC,IAAI;aACM,CAAA;YACtB,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;gBACZ,iBAAiB,CAAC,GAAG,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAA,MAAA,GAAG,CAAC,GAAG,0CAAE,GAAG,mCAAI,EAAE,CAAC,CAAA;gBAClE,iBAAiB,CAAC,IAAI,GAAG,CAAA,MAAA,GAAG,CAAC,GAAG,0CAAE,IAAI,EAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAA,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,GAAG,0CAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;YAC7F,CAAC;YACD,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,iBAAiB,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;gBACpB,iBAAiB,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,CAAA;YAC7C,CAAC;YACD,IAAI,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC;gBAC3B,iBAAiB,CAAC,cAAc,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAA;YAC3D,CAAC;YAKD,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;gBAClC,iBAAiB,CAAC,WAAW,GAAG,iBAAiB,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;YAC9E,CAAC;YACD,IAAI,GAAG,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;gBACzC,iBAAiB,CAAC,kBAAkB,GAAG,iBAAiB,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;YAC5F,CAAC;YACD,IAAI,GAAG,CAAC,wBAAwB,EAAE,CAAC;gBACjC,iBAAiB,mCACZ,iBAAiB,GACjB,GAAG,CAAC,wBAAwB,CAChC,CAAA;gBACD,IAAI,iBAAiB,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;oBACvD,iBAAiB,CAAC,kBAAkB,GAAG,iBAAiB,CAAC,SAAS,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAA;gBAC1G,CAAC;gBACD,IAAI,iBAAiB,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;oBAChD,iBAAiB,CAAC,WAAW,GAAG,iBAAiB,CAAC,SAAS,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAA;gBAC5F,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,iBAAiB,CAAA;IAC1B,CAAC;CACF;AAnGD,8CAmGC","sourcesContent":["import { ConnectionOptions, TlsOptions } from 'tls'\nimport { ITlsOptions } from './tls-options'\nimport { ITcpTransportDescription } from './tcp-transport-description'\nconst path = require('path')\nconst fs = require('fs')\n\nexport class TlsOptionsFactory {\n /*\n session descriptions are routinely hand written json, where a boolean flag is easily\n quoted by mistake i.e. \"rejectUnauthorized\": \"false\". node treats that string as truthy\n and so does exactly the opposite of what was intended, silently. accept the two string\n spellings rather than inverting the caller's meaning - anything else is left alone so it\n shows up as an obviously wrong value rather than being guessed at.\n */\n static asBoolean (value: any): any {\n if (typeof value === 'string') {\n const lowered = value.toLowerCase()\n if (lowered === 'true') return true\n if (lowered === 'false') return false\n }\n return value\n }\n\n static read (filePath: string): string {\n const root: string = path.join(__dirname, '../../../')\n const fullPath = path.join(root, filePath)\n return fs.readFileSync(fullPath,\n {\n encoding: 'utf8', flag: 'r'\n })\n }\n\n static getTlsOptions (tls: ITlsOptions): TlsOptions | null {\n let tlsOptions: TlsOptions | null = null\n if (tls) {\n tlsOptions = {\n requestCert: TlsOptionsFactory.asBoolean(tls.requestCert),\n rejectUnauthorized: TlsOptionsFactory.asBoolean(tls.rejectUnauthorized)\n } as TlsOptions\n\n if (tls.key) {\n tlsOptions.key = TlsOptionsFactory.read(tls.key)\n tlsOptions.cert = tls?.cert ? TlsOptionsFactory.read(tls?.cert) : undefined\n }\n\n if (tls.ca && tls.ca.length > 0) {\n tlsOptions.ca = tls.ca.map(i => TlsOptionsFactory.read(i))\n }\n\n if (tls.nodeTlsServerOptions) {\n tlsOptions = {\n ...tlsOptions,\n ...tls.nodeTlsServerOptions\n }\n }\n }\n return tlsOptions\n }\n\n static getTlsConnectionOptions (tcp: ITcpTransportDescription): ConnectionOptions | null {\n let connectionOptions: ConnectionOptions | null = null\n const tls = tcp.tls\n if (tls) {\n connectionOptions = {\n port: tcp.port,\n host: tcp.host\n } as ConnectionOptions\n if (tls.key) {\n connectionOptions.key = TlsOptionsFactory.read(tcp.tls?.key ?? '')\n connectionOptions.cert = tcp.tls?.cert ? TlsOptionsFactory.read(tcp?.tls?.cert) : undefined\n }\n if (tcp.tls.ca && tcp.tls.ca.length > 0) {\n connectionOptions.ca = tcp.tls.ca.map(i => TlsOptionsFactory.read(i))\n }\n if (tcp.tls.timeout) {\n connectionOptions.timeout = tcp.tls.timeout\n }\n if (tcp.tls.sessionTimeout) {\n connectionOptions.sessionTimeout = tcp.tls.sessionTimeout\n }\n // https://github.com/TimelordUK/jspurefix/issues/151 - ITlsOptions declares these and\n // getTlsOptions honours them on the server path, so map them here too rather than making\n // nodeTlsConnectionOptions the only spelling that works for an initiator. Applied before\n // the spread below so an explicit nodeTlsConnectionOptions value still wins.\n if (tls.requestCert !== undefined) {\n connectionOptions.requestCert = TlsOptionsFactory.asBoolean(tls.requestCert)\n }\n if (tls.rejectUnauthorized !== undefined) {\n connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(tls.rejectUnauthorized)\n }\n if (tls.nodeTlsConnectionOptions) {\n connectionOptions = {\n ...connectionOptions,\n ...tls.nodeTlsConnectionOptions\n }\n if (connectionOptions.rejectUnauthorized !== undefined) {\n connectionOptions.rejectUnauthorized = TlsOptionsFactory.asBoolean(connectionOptions.rejectUnauthorized)\n }\n if (connectionOptions.requestCert !== undefined) {\n connectionOptions.requestCert = TlsOptionsFactory.asBoolean(connectionOptions.requestCert)\n }\n }\n }\n return connectionOptions\n }\n}\n"]}
|
package/docs/acceptor.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Running a FIX acceptor
|
|
2
|
+
|
|
3
|
+
The acceptor in jspurefix began as a smoke-test harness for the initiator — enough
|
|
4
|
+
to answer a Logon so the client side could be exercised. It is now used to host real
|
|
5
|
+
counterparties, and this document describes the model it follows, what it guarantees,
|
|
6
|
+
and what the log tells you.
|
|
7
|
+
|
|
8
|
+
Much of this is a back-port of
|
|
9
|
+
[cspurefix](https://github.com/TimelordUK/cspurefix), where the acceptor was hardened
|
|
10
|
+
against real brokers.
|
|
11
|
+
|
|
12
|
+
## Transports and sessions are not the same thing
|
|
13
|
+
|
|
14
|
+
A **transport** is one TCP connection. A **session** is one FIX conversation,
|
|
15
|
+
identified by `SessionId` = `BeginString`-`SenderCompID`-`TargetCompID`, and owning
|
|
16
|
+
the sequence numbers and the message store behind that conversation.
|
|
17
|
+
|
|
18
|
+
The relationship is one session per transport, but a session **outlives** the
|
|
19
|
+
connection it started on: a counterparty that drops and reconnects is resuming the
|
|
20
|
+
same FIX session, from the same persisted sequence state, on a new transport.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
listener (TcpAcceptorListener)
|
|
24
|
+
└── acceptor (TcpAcceptor) one net/tls Server, one SessionRegistry
|
|
25
|
+
├── transport 1 ──► session scope ──► FixSession ──► SessionId A ──► store A
|
|
26
|
+
├── transport 2 ──► session scope ──► FixSession ──► SessionId B ──► store B
|
|
27
|
+
└── transport 3 ──► session scope ──► FixSession ──► SessionId A ──► store A
|
|
28
|
+
(replaces the first)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Session scopes
|
|
32
|
+
|
|
33
|
+
Every accepted connection gets its own DI child container — a *session scope* —
|
|
34
|
+
carrying its own:
|
|
35
|
+
|
|
36
|
+
- parse buffer and transmit buffer (`ElasticBuffer`)
|
|
37
|
+
- `ISessionDescription`, and therefore its own `SessionId` and message store
|
|
38
|
+
- session message factory, so outbound headers carry that session's comp ids
|
|
39
|
+
- `IJsFixConfig`, which is what the application's session receives
|
|
40
|
+
|
|
41
|
+
This matters because those are otherwise process-wide singletons on the session
|
|
42
|
+
container. Two clients on one listener would parse into the same buffer and compute
|
|
43
|
+
the same `SessionId`.
|
|
44
|
+
|
|
45
|
+
Applications opt in by using the config they are **handed**, not the one they
|
|
46
|
+
captured:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
protected override makeFactory (config: IJsFixConfig): EngineFactory {
|
|
50
|
+
const isInitiator = this.isInitiator(config.description)
|
|
51
|
+
return {
|
|
52
|
+
// sessionConfig is this connection's own scope
|
|
53
|
+
makeSession: (sessionConfig: IJsFixConfig) => isInitiator
|
|
54
|
+
? new MyClient(sessionConfig)
|
|
55
|
+
: new MyServer(sessionConfig)
|
|
56
|
+
} as EngineFactory
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Writing `makeSession: () => new MyServer(config)` closes over the launcher's config
|
|
61
|
+
and opts the application out of per-session isolation. It still works for a single
|
|
62
|
+
counterparty; it does not work for several.
|
|
63
|
+
|
|
64
|
+
### Custom session message factories
|
|
65
|
+
|
|
66
|
+
If you supply your own `ISessionMsgFactory` — usually by overriding
|
|
67
|
+
`SessionContainer.makeSessionFactory` to stamp extra header fields — the scope needs
|
|
68
|
+
a copy of *your* factory bound to that session's description, not a stock one.
|
|
69
|
+
|
|
70
|
+
Extending `ASessionMsgFactory` (or `AsciiSessionMsgFactory` / `FixmlSessionMsgFactory`)
|
|
71
|
+
gets this for free: the default `cloneFor` reconstructs through the concrete
|
|
72
|
+
constructor, so a subclass taking `(description, mutator)` needs no extra work.
|
|
73
|
+
Override `cloneFor` if your constructor takes anything else:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
class BrokerMsgFactory extends AsciiSessionMsgFactory {
|
|
77
|
+
constructor (description: ISessionDescription, private readonly desk: string) {
|
|
78
|
+
super(description)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// the default clone would not know about `desk`
|
|
82
|
+
public override cloneFor (description: ISessionDescription): ISessionMsgFactory {
|
|
83
|
+
return new BrokerMsgFactory(description, this.desk)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## The session registry
|
|
89
|
+
|
|
90
|
+
FIX permits exactly one active session per `SessionId`. `SessionRegistry` enforces
|
|
91
|
+
that: a session claims its `SessionId` once its store is initialised, and any
|
|
92
|
+
*different* session already holding it is stopped first.
|
|
93
|
+
|
|
94
|
+
This is what answers the half-open socket problem. A counterparty whose network path
|
|
95
|
+
disappears without FIN or RST leaves the acceptor holding a socket it believes is
|
|
96
|
+
alive. When that counterparty reconnects and logs on, the registry stops the stale
|
|
97
|
+
session before the new one proceeds — otherwise two sessions write to one store and
|
|
98
|
+
the sequence numbers diverge beyond recovery.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
[SessionRegistry] FOUND EXISTING SESSION for FIX4.4-accept-comp-init-comp - this is a reconnection scenario
|
|
102
|
+
[SessionRegistry] stopping old session to prevent stale transport writes
|
|
103
|
+
[...FixSession] requestStop: replaced by new connection - stopping old session with stale transport
|
|
104
|
+
[SessionRegistry] session NOT unregistered (already replaced by new connection or not found): ...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
That last line is expected, not an error: the displaced session unregisters on its
|
|
108
|
+
way out, and the identity check stops it evicting its successor.
|
|
109
|
+
|
|
110
|
+
The listener creates a registry per listener. Supply your own via
|
|
111
|
+
`config.sessionRegistry` if you want to inspect it — `count`, `keys()` and `get()`
|
|
112
|
+
are read-only views over the live population.
|
|
113
|
+
|
|
114
|
+
An application can stop a session itself with `session.requestStop(reason)`.
|
|
115
|
+
|
|
116
|
+
## Wildcard TargetCompID
|
|
117
|
+
|
|
118
|
+
An acceptor that sets `"TargetCompID": "*"` does not need to know its counterparties
|
|
119
|
+
in advance. On the first Logon the session adopts the peer's `SenderCompID` as its
|
|
120
|
+
target and only then builds the identity-derived state: `SessionId`, message store,
|
|
121
|
+
sequence coordinator, registry claim, and its logger name.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"SenderCompId": "accept-comp",
|
|
126
|
+
"TargetCompID": "*",
|
|
127
|
+
"store": { "type": "file", "directory": "store/acceptor" }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Binding is deferred rather than done at construction so that each counterparty gets
|
|
132
|
+
its **own** store file — `FIX4.4-accept-comp-<peer>.seqnums` — which is what makes
|
|
133
|
+
persisted recovery work for a multi-client acceptor. (cspurefix binds in the
|
|
134
|
+
constructor and so keys a wildcard store on the literal `*`; that is fine for
|
|
135
|
+
in-memory stores only.)
|
|
136
|
+
|
|
137
|
+
Messages that arrive while the bind is in flight are queued and replayed in order.
|
|
138
|
+
`*` on an initiator is rejected at construction — an initiator has to know who it is
|
|
139
|
+
calling.
|
|
140
|
+
|
|
141
|
+
## Detecting a peer that has gone
|
|
142
|
+
|
|
143
|
+
Three independent mechanisms, because no single one is sufficient:
|
|
144
|
+
|
|
145
|
+
| Level | Mechanism | Detects |
|
|
146
|
+
|-------|-----------|---------|
|
|
147
|
+
| Transport | TCP keep-alive (`tcp.keepAliveMs`, default 30s, `0` disables) | a peer whose host or network path has vanished |
|
|
148
|
+
| Protocol | `TestRequest` on heartbeat timeout, then terminate | a peer that is connected but not responding |
|
|
149
|
+
| Session | `SessionRegistry` replacing on the next Logon | a peer that has already reconnected |
|
|
150
|
+
|
|
151
|
+
`MsgTransport.end()` performs a graceful close and then destroys the socket after
|
|
152
|
+
`MsgTransport.lingerMs` (default 5s), so an unanswered FIN cannot pin the handle
|
|
153
|
+
indefinitely. `TcpAcceptor.close()` releases live connections rather than waiting on
|
|
154
|
+
them — `net.Server.close` otherwise blocks on whoever happens to be connected.
|
|
155
|
+
|
|
156
|
+
## Reading the log
|
|
157
|
+
|
|
158
|
+
The lifecycle of one connection, in order:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
[TcpAcceptor] net creates session 2
|
|
162
|
+
[TcpAcceptor] transport 2 keep-alive enabled, initial delay 30000ms
|
|
163
|
+
[TcpAcceptor] transport 2 from ::1:55642 given its own session scope
|
|
164
|
+
[TcpAcceptor] new transport id = 2 from ::1:55642 created total transports = 2
|
|
165
|
+
[acceptor] transport 2: creating session via DI token FixSession [accept-comp -> *]
|
|
166
|
+
[acceptor] acceptor census: transports=2 [...] sessions=1 [...]
|
|
167
|
+
[...] wildcard acceptor: awaiting peer Logon to establish session identity
|
|
168
|
+
[...] wildcard acceptor: binding session identity to peer SenderCompID 'init-comp_2'
|
|
169
|
+
[...] session identity bound: FIX4.4-accept-comp-init-comp_2 nextSender=1, expectedTarget=1
|
|
170
|
+
[SessionRegistry] no existing session found - registering new session: FIX4.4-accept-comp-init-comp_2
|
|
171
|
+
...
|
|
172
|
+
[SessionRegistry] successfully unregistered session: ..., remainingActiveSessions=1
|
|
173
|
+
[TcpAcceptor] transport 2 from ::1:55642 harvested after 18s (socket closed) total transports = 1
|
|
174
|
+
[acceptor] acceptor census: transports=1 [...] sessions=1 [...]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The **census** line is emitted whenever the population changes and answers "how many
|
|
178
|
+
counterparties are connected, and which sessions are claimed" from the log alone.
|
|
179
|
+
Transports and sessions can legitimately differ for a moment: a connection that has
|
|
180
|
+
not logged on yet has a transport and no session; a session being replaced has both
|
|
181
|
+
counted until its socket is released.
|
|
182
|
+
|
|
183
|
+
Once a wildcard session has bound, its log prefix carries the counterparty
|
|
184
|
+
(`[my_server:init-comp_2:FixSession]`), so a multi-client server log can be read one
|
|
185
|
+
counterparty at a time.
|
|
186
|
+
|
|
187
|
+
## Server-side recovery from a persisted store
|
|
188
|
+
|
|
189
|
+
With `"store": { "type": "file", "directory": "..." }` the acceptor persists, per
|
|
190
|
+
`SessionId`, the next sender sequence and the last processed peer sequence, plus the
|
|
191
|
+
sent message bodies for replay.
|
|
192
|
+
|
|
193
|
+
On a reconnect the session reads that store before responding to the Logon, so it
|
|
194
|
+
knows what it last sent and what it last received. If the peer's Logon sequence is
|
|
195
|
+
ahead of what the store expects, the acceptor issues a `ResendRequest`; if the peer
|
|
196
|
+
asks for a resend, the stored bodies are replayed with `PossDupFlag=Y`.
|
|
197
|
+
`ResetSeqNumFlag=Y` from either side clears the store and restarts at 1.
|
|
198
|
+
|
|
199
|
+
Because the store is keyed on `SessionId`, a wildcard acceptor recovers each
|
|
200
|
+
counterparty independently.
|
|
201
|
+
|
|
202
|
+
## See also
|
|
203
|
+
|
|
204
|
+
- [`src/transport/tcp/tcp-acceptor.ts`](../src/transport/tcp/tcp-acceptor.ts) — listener, transport lifecycle
|
|
205
|
+
- [`src/transport/session/session-registry.ts`](../src/transport/session/session-registry.ts) — one session per SessionId
|
|
206
|
+
- [`src/runtime/session-scope.ts`](../src/runtime/session-scope.ts) — per-connection DI scope
|
|
207
|
+
- [jspf-demo](https://github.com/TimelordUK/jspf-demo) — `multi-client` and `stale-transport` scenarios
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Benchmarking
|
|
2
|
+
|
|
3
|
+
`jsfix-bench` drives fix bytes through the real parser and reports what the work
|
|
4
|
+
costs in time and in garbage. It exists so a change to a hot path can be argued
|
|
5
|
+
about with a number rather than an opinion.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm run build
|
|
9
|
+
npm run bench
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Always under `--expose-gc`, which the npm scripts do for you. Without it rounds
|
|
13
|
+
cannot be isolated from one another and the retained heap figures mean very
|
|
14
|
+
little; the report says so at the top when it is missing.
|
|
15
|
+
|
|
16
|
+
## What it measures
|
|
17
|
+
|
|
18
|
+
Two cases by default, over the same input:
|
|
19
|
+
|
|
20
|
+
| case | what it covers |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `parse:view` | tokenise, discover the structure, build the `MsgView` |
|
|
23
|
+
| `parse:object` | the above, then `toObject()` to materialise the object graph |
|
|
24
|
+
|
|
25
|
+
Both matter. `parse:view` is the engine's own cost. `parse:object` is what a
|
|
26
|
+
drop copy consumer actually pays, and the gap between them is the price of
|
|
27
|
+
materialising a message rather than reading fields out of the view.
|
|
28
|
+
|
|
29
|
+
## Reading the report
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
parse:object
|
|
33
|
+
rounds 8 x 4000 msgs (warmup 4000)
|
|
34
|
+
per msg median 239.922us min 231.305us p95 242.623us max 242.623us
|
|
35
|
+
throughput 4,168 msg/sec
|
|
36
|
+
noise 1.80% of median (a change smaller than this means nothing)
|
|
37
|
+
gc pass 4000 msgs uninterrupted: 25 minor 0 major 5.8ms paused
|
|
38
|
+
gc rate 6250.0 minor/M msgs 0.0 major/M msgs 1445ms paused/M msgs
|
|
39
|
+
retained -0.00MB -0.6 bytes/msg heap at end of pass 116.08MB
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **rounds** each contribute one timing sample. One long run cannot tell you
|
|
43
|
+
whether a number repeats.
|
|
44
|
+
- **min** is the closest reading to the true cost of the work - a round can only
|
|
45
|
+
be made slower by interference, never faster - while **median** is what to
|
|
46
|
+
expect in practice.
|
|
47
|
+
- **noise** is the run to run spread. Treat it as the resolution of the
|
|
48
|
+
instrument: a change smaller than this is not evidence of anything.
|
|
49
|
+
- **gc rate** is collections per million messages. Node will not tell you how
|
|
50
|
+
many bytes a stretch of code allocated, so scavenge frequency is the stand in.
|
|
51
|
+
In practice it is far more repeatable than the timings - the same build tends
|
|
52
|
+
to reproduce it exactly - which makes it the more sensitive of the two signals
|
|
53
|
+
for spotting an allocation change.
|
|
54
|
+
- **retained** is what the pass left behind after a forced collection, not what
|
|
55
|
+
it churned through. A steady state parser should sit near zero. Churn is what
|
|
56
|
+
the gc figures are for.
|
|
57
|
+
|
|
58
|
+
## Why timing and memory are measured separately
|
|
59
|
+
|
|
60
|
+
Isolating a timed round means forcing a collection between rounds, and a forced
|
|
61
|
+
collection is itself a gc event - counting those would report the harness rather
|
|
62
|
+
than the engine. So the timed rounds collect between them and report no gc
|
|
63
|
+
figures, and a separate uninterrupted pass runs with the probe attached and
|
|
64
|
+
reports no timings. BenchmarkDotNet splits its memory diagnoser out of the
|
|
65
|
+
timed iterations for the same reason.
|
|
66
|
+
|
|
67
|
+
Node also dispatches a gc entry on the turn *after* the collection, and the
|
|
68
|
+
benchmark loop is synchronous, so the runner yields until delivery goes quiet
|
|
69
|
+
before reading the totals. A single yield loses most of them.
|
|
70
|
+
|
|
71
|
+
## Comparing against a baseline
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
npm run build
|
|
75
|
+
node --expose-gc dist/jsfix-bench --save=benchmarks/baseline.json --label="before"
|
|
76
|
+
# ... make a change, rebuild ...
|
|
77
|
+
node --expose-gc dist/jsfix-bench --baseline=benchmarks/baseline.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
parse:object
|
|
82
|
+
per msg 239.922us -> 243.523us +1.50% noise (noise floor +/-2.75%)
|
|
83
|
+
minor gc 6250.0 -> 6250.0 per M msgs +0.00%
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A difference is only called `FASTER` or `SLOWER` when it is larger than the
|
|
87
|
+
spread both runs displayed. Anything else is reported as `noise`, because that
|
|
88
|
+
is what it is. Baselines are specific to the machine and node build that took
|
|
89
|
+
them - the report warns when they differ - so `benchmarks/` is not committed.
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
| flag | meaning |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `--fix=<path>` | fix log to parse |
|
|
96
|
+
| `--dict=<name>` | dictionary, e.g. `qf44`, `repo44`, `qf50sp2` |
|
|
97
|
+
| `--delimiter=<char>` | field delimiter in the log, defaults to `\|` |
|
|
98
|
+
| `--depth=view,object` | which stages to measure |
|
|
99
|
+
| `--rounds=<n>` | measured rounds, one sample each |
|
|
100
|
+
| `--msgs=<n>` | messages per round |
|
|
101
|
+
| `--warmup=<n>` | messages discarded before measuring |
|
|
102
|
+
| `--save=<path>` | write the suite to json |
|
|
103
|
+
| `--baseline=<path>` | compare against a saved suite |
|
|
104
|
+
| `--json` | print the suite as json instead of a report |
|
|
105
|
+
|
|
106
|
+
Ready made suites: `bench:er`, `bench:er2`, `bench:nos`, `bench:md`, `bench:tc`.
|
|
107
|
+
|
|
108
|
+
Message counts are rounded up to a whole number of passes over the input file,
|
|
109
|
+
since a pass cannot be stopped part way. A log holding one message gives the
|
|
110
|
+
finest control.
|
|
111
|
+
|
|
112
|
+
## Warmup
|
|
113
|
+
|
|
114
|
+
The default warmup is 20,000 messages. V8 needs to see a function run several
|
|
115
|
+
thousand times before it stops interpreting it, so an unwarmed first round
|
|
116
|
+
measures the wrong code. If you lower it, check the noise figure has not risen -
|
|
117
|
+
that is what an under-warmed run looks like.
|
package/eslint.config.mjs
CHANGED
|
@@ -106,6 +106,8 @@ export default [
|
|
|
106
106
|
'prefer-template': 'off',
|
|
107
107
|
'radix': 'off',
|
|
108
108
|
'require-unicode-regexp': 'off',
|
|
109
|
+
// named capture groups need a target of es2018, this package emits es6
|
|
110
|
+
'prefer-named-capture-group': 'off',
|
|
109
111
|
'promise/avoid-new': 'off',
|
|
110
112
|
'promise/no-multiple-resolved': 'off',
|
|
111
113
|
'import/enforce-node-protocol-usage': 'off',
|
|
@@ -114,6 +116,18 @@ export default [
|
|
|
114
116
|
}
|
|
115
117
|
},
|
|
116
118
|
{
|
|
117
|
-
|
|
119
|
+
files: ['src/test/**/*.ts'],
|
|
120
|
+
rules: {
|
|
121
|
+
// tests assert a value is present with expect(x).toBeDefined() and then use x!.
|
|
122
|
+
// the compiler cannot follow that, and rewriting each site as an explicit throw
|
|
123
|
+
// buries the assertion the test is actually about.
|
|
124
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
125
|
+
// save/restore of a static around an awaited body is not a race in a jest file,
|
|
126
|
+
// which runs its tests serially.
|
|
127
|
+
'require-atomic-updates': 'off'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
ignores: ['dist/**', 'node_modules/**', 'coverage/**']
|
|
118
132
|
}
|
|
119
133
|
]
|