@homaiohq/napi-quickfix 0.0.0-stage → 0.2.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/CMakeLists.txt +135 -0
- package/LICENSE +21 -0
- package/README.md +638 -2
- package/THIRD_PARTY_LICENSES +48 -0
- package/cpp/acceptor_wrap.cpp +468 -0
- package/cpp/acceptor_wrap.h +116 -0
- package/cpp/addon.cpp +87 -0
- package/cpp/application_bridge.cpp +466 -0
- package/cpp/application_bridge.h +204 -0
- package/cpp/data_dictionary_wrap.cpp +165 -0
- package/cpp/data_dictionary_wrap.h +59 -0
- package/cpp/engine_common.cpp +5 -0
- package/cpp/engine_common.h +127 -0
- package/cpp/engine_workers.h +167 -0
- package/cpp/errors.h +142 -0
- package/cpp/field_map_ops.h +492 -0
- package/cpp/group_wrap.cpp +221 -0
- package/cpp/group_wrap.h +76 -0
- package/cpp/initiator_wrap.cpp +492 -0
- package/cpp/initiator_wrap.h +104 -0
- package/cpp/message_wrap.cpp +288 -0
- package/cpp/message_wrap.h +95 -0
- package/cpp/session_id_wrap.cpp +91 -0
- package/cpp/session_id_wrap.h +46 -0
- package/cpp/session_op_gate.h +201 -0
- package/cpp/session_settings_wrap.cpp +98 -0
- package/cpp/session_settings_wrap.h +40 -0
- package/cpp/session_static.cpp +220 -0
- package/cpp/session_wrap.cpp +438 -0
- package/cpp/session_wrap.h +144 -0
- package/dist/cjs/acceptor.d.ts +29 -0
- package/dist/cjs/acceptor.js +38 -0
- package/dist/cjs/application.d.ts +115 -0
- package/dist/cjs/application.js +62 -0
- package/dist/cjs/data-dictionary.d.ts +67 -0
- package/dist/cjs/data-dictionary.js +105 -0
- package/dist/cjs/engine.d.ts +152 -0
- package/dist/cjs/engine.js +183 -0
- package/dist/cjs/enums.d.ts +56 -0
- package/dist/cjs/enums.js +44 -0
- package/dist/cjs/field-value.d.ts +12 -0
- package/dist/cjs/field-value.js +40 -0
- package/dist/cjs/generated/fields.d.ts +6122 -0
- package/dist/cjs/generated/fields.js +6127 -0
- package/dist/cjs/generated/values.d.ts +8548 -0
- package/dist/cjs/generated/values.js +8580 -0
- package/dist/cjs/group.d.ts +126 -0
- package/dist/cjs/group.js +185 -0
- package/dist/cjs/index.d.ts +68 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/initiator.d.ts +29 -0
- package/dist/cjs/initiator.js +38 -0
- package/dist/cjs/load-native.cjs +5 -0
- package/dist/cjs/message.d.ts +238 -0
- package/dist/cjs/message.js +299 -0
- package/dist/cjs/native.d.ts +325 -0
- package/dist/cjs/native.js +22 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/session-id.d.ts +51 -0
- package/dist/cjs/session-id.js +87 -0
- package/dist/cjs/session-settings.d.ts +46 -0
- package/dist/cjs/session-settings.js +75 -0
- package/dist/cjs/session.d.ts +213 -0
- package/dist/cjs/session.js +337 -0
- package/dist/esm/acceptor.d.ts +29 -0
- package/dist/esm/acceptor.js +34 -0
- package/dist/esm/application.d.ts +115 -0
- package/dist/esm/application.js +57 -0
- package/dist/esm/data-dictionary.d.ts +67 -0
- package/dist/esm/data-dictionary.js +101 -0
- package/dist/esm/engine.d.ts +152 -0
- package/dist/esm/engine.js +179 -0
- package/dist/esm/enums.d.ts +56 -0
- package/dist/esm/enums.js +35 -0
- package/dist/esm/field-value.d.ts +12 -0
- package/dist/esm/field-value.js +37 -0
- package/dist/esm/generated/fields.d.ts +6122 -0
- package/dist/esm/generated/fields.js +6124 -0
- package/dist/esm/generated/values.d.ts +8548 -0
- package/dist/esm/generated/values.js +8564 -0
- package/dist/esm/group.d.ts +126 -0
- package/dist/esm/group.js +181 -0
- package/dist/esm/index.d.ts +68 -0
- package/dist/esm/index.js +43 -0
- package/dist/esm/initiator.d.ts +29 -0
- package/dist/esm/initiator.js +34 -0
- package/dist/esm/load-native.cjs +5 -0
- package/dist/esm/message.d.ts +238 -0
- package/dist/esm/message.js +293 -0
- package/dist/esm/native.d.ts +325 -0
- package/dist/esm/native.js +16 -0
- package/dist/esm/session-id.d.ts +51 -0
- package/dist/esm/session-id.js +82 -0
- package/dist/esm/session-settings.d.ts +46 -0
- package/dist/esm/session-settings.js +71 -0
- package/dist/esm/session.d.ts +213 -0
- package/dist/esm/session.js +329 -0
- package/package.json +95 -4
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/prebuilds/linux-x64/node.napi.glibc.node +0 -0
- package/prebuilds/linux-x64/node.napi.musl.node +0 -0
- package/prebuilds/win32-x64/node.napi.node +0 -0
- package/src/load-native.cjs +5 -0
package/README.md
CHANGED
|
@@ -1,3 +1,639 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @homaiohq/napi-quickfix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/homaiohq/napi-quickfix/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@homaiohq/napi-quickfix)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
A modern **Node-API** (N-API / `node-addon-api`) C++ binding for the
|
|
9
|
+
[QuickFIX](https://github.com/quickfix/quickfix) FIX-protocol engine. It exposes
|
|
10
|
+
both the *pure* message layer (build, parse, and validate FIX messages) and the
|
|
11
|
+
full *session engine* (socket initiator/acceptor with `FIX::Application`
|
|
12
|
+
callbacks bridged to JavaScript) — with prebuilt binaries so most consumers need
|
|
13
|
+
**no C++ compiler**, dual **ESM + CommonJS** entry points, and TypeScript types
|
|
14
|
+
included.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Modern Node-API** — built on `node-addon-api` (N-API v9), not the legacy
|
|
19
|
+
NAN/node-gyp stack. One prebuild per platform serves every supported Node LTS.
|
|
20
|
+
- **No compiler needed** — prebuilt native binaries ship in the npm package (via
|
|
21
|
+
`prebuildify` + `node-gyp-build`); a source build via CMake is only a fallback.
|
|
22
|
+
- **Dual ESM + CJS** — a proper `exports` map with `import` and `require` entry
|
|
23
|
+
points.
|
|
24
|
+
- **TypeScript types included** — hand-written `.d.ts` for the full public API.
|
|
25
|
+
- **Full FIX surface** — the session engine (`Initiator`/`Acceptor` +
|
|
26
|
+
synchronous application handlers), per-session control (`Session`: sequence
|
|
27
|
+
numbers, logon/logout, runtime options) *and* the pure layer (`Message` with
|
|
28
|
+
repeating `Group`s and dictionary-aware parsing, `SessionSettings`,
|
|
29
|
+
`DataDictionary`, `SessionID`).
|
|
30
|
+
- **Ergonomic, safe API** — chainable message building with automatic
|
|
31
|
+
header/trailer field routing, typed rejections, and C++ exceptions surfaced as
|
|
32
|
+
JavaScript `Error`s.
|
|
33
|
+
- **Every FIX constant, typed** — `FIELD` (tag numbers) and one value group per
|
|
34
|
+
field (`MsgType`, `Side`, `ExecType`, `OrdStatus`, ...) are
|
|
35
|
+
generated from the bundled QuickFIX headers with literal types, frozen, and
|
|
36
|
+
importable without loading the native addon.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
yarn add @homaiohq/napi-quickfix
|
|
42
|
+
# or
|
|
43
|
+
npm install @homaiohq/napi-quickfix
|
|
44
|
+
# or
|
|
45
|
+
pnpm add @homaiohq/napi-quickfix
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prebuilt binaries are published for **Linux x64** (both **glibc** and
|
|
49
|
+
**musl/Alpine**), **macOS** x64 + arm64, and **Windows** x64. On these platforms
|
|
50
|
+
installation requires no toolchain.
|
|
51
|
+
|
|
52
|
+
The two Linux binaries are libc-tagged and picked automatically: `node-gyp-build`
|
|
53
|
+
selects the musl build when `/etc/alpine-release` exists. On a musl distro that
|
|
54
|
+
is *not* Alpine, set `LIBC=musl` to force it.
|
|
55
|
+
|
|
56
|
+
If a matching prebuild is not available for your platform/arch, the install
|
|
57
|
+
falls back to building from source, which requires:
|
|
58
|
+
|
|
59
|
+
- **CMake** (>= 3.12)
|
|
60
|
+
- A **C++17** compiler (GCC / Clang / MSVC)
|
|
61
|
+
|
|
62
|
+
On Alpine/musl that is `apk add build-base cmake git` — `build-base` supplies
|
|
63
|
+
g++/make, and `git` is needed for the QuickFIX `FetchContent` clone.
|
|
64
|
+
|
|
65
|
+
QuickFIX itself is fetched at build time via CMake `FetchContent` (pinned to
|
|
66
|
+
`v1.16.0`) — nothing is vendored or committed to this repository.
|
|
67
|
+
|
|
68
|
+
Releases are published from GitHub Actions via npm Trusted Publishing with
|
|
69
|
+
[provenance](https://docs.npmjs.com/generating-provenance-statements), so you can
|
|
70
|
+
check that an installed version was built from this repository:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
npm audit signatures
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
### ESM
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import {
|
|
82
|
+
Message,
|
|
83
|
+
SessionSettings,
|
|
84
|
+
Initiator,
|
|
85
|
+
FixReject,
|
|
86
|
+
sendToTarget,
|
|
87
|
+
FIELD,
|
|
88
|
+
MsgType,
|
|
89
|
+
} from '@homaiohq/napi-quickfix';
|
|
90
|
+
|
|
91
|
+
// Build a message. Header/trailer fields (e.g. MsgType) auto-route to the right
|
|
92
|
+
// section, so setField is all you usually need. setField is chainable.
|
|
93
|
+
const order = new Message()
|
|
94
|
+
.setField(FIELD.MsgType, MsgType.NewOrderSingle)
|
|
95
|
+
.setField(FIELD.ClOrdID, 'order-1')
|
|
96
|
+
.setField(FIELD.Symbol, 'AAPL')
|
|
97
|
+
.setField(FIELD.OrderQty, 100);
|
|
98
|
+
|
|
99
|
+
console.log(order.getMsgType()); // 'D'
|
|
100
|
+
console.log(order.toPretty());
|
|
101
|
+
|
|
102
|
+
// Parse a raw FIX wire string back into a Message.
|
|
103
|
+
const parsed = Message.parse(order.toString());
|
|
104
|
+
|
|
105
|
+
// Load session settings from an in-memory config.
|
|
106
|
+
const settings = SessionSettings.fromString(`
|
|
107
|
+
[DEFAULT]
|
|
108
|
+
ConnectionType=initiator
|
|
109
|
+
SocketConnectHost=127.0.0.1
|
|
110
|
+
SocketConnectPort=5001
|
|
111
|
+
HeartBtInt=30
|
|
112
|
+
UseDataDictionary=N
|
|
113
|
+
StartTime=00:00:00
|
|
114
|
+
EndTime=00:00:00
|
|
115
|
+
|
|
116
|
+
[SESSION]
|
|
117
|
+
BeginString=FIX.4.4
|
|
118
|
+
SenderCompID=CLIENT
|
|
119
|
+
TargetCompID=BROKER
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
// Create the initiator with synchronous application handlers.
|
|
123
|
+
const initiator = new Initiator({
|
|
124
|
+
settings,
|
|
125
|
+
store: 'memory',
|
|
126
|
+
log: 'none',
|
|
127
|
+
handlers: {
|
|
128
|
+
onLogon(sessionID) {
|
|
129
|
+
console.log('logged on:', sessionID.toString());
|
|
130
|
+
// sessionID.toString() -> "FIX.4.4:CLIENT->BROKER"
|
|
131
|
+
sendToTarget(order, sessionID);
|
|
132
|
+
},
|
|
133
|
+
toAdmin(msg) {
|
|
134
|
+
// Handlers run synchronously and may MUTATE the outbound message.
|
|
135
|
+
if (msg.getMsgType() === MsgType.Logon) {
|
|
136
|
+
msg.setField(FIELD.Username, 'user');
|
|
137
|
+
msg.setField(FIELD.Password, 'secret');
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
fromApp(msg) {
|
|
141
|
+
// Reject an inbound message by THROWING a typed rejection.
|
|
142
|
+
if (msg.getMsgType() === 'X') {
|
|
143
|
+
throw new FixReject('UnsupportedMessageType');
|
|
144
|
+
}
|
|
145
|
+
console.log('received:', msg.getMsgType());
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Observe-only events (cannot mutate/reject) are also emitted.
|
|
151
|
+
initiator.on('logout', (id) => console.log('logged out:', id.toString()));
|
|
152
|
+
|
|
153
|
+
initiator.start();
|
|
154
|
+
|
|
155
|
+
// ... later, shut down cleanly (drain gracefully).
|
|
156
|
+
process.on('SIGINT', () => {
|
|
157
|
+
initiator.stop();
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### CommonJS
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const {
|
|
165
|
+
Message,
|
|
166
|
+
SessionSettings,
|
|
167
|
+
Initiator,
|
|
168
|
+
FIELD,
|
|
169
|
+
MsgType,
|
|
170
|
+
} = require('@homaiohq/napi-quickfix');
|
|
171
|
+
|
|
172
|
+
const order = new Message()
|
|
173
|
+
.setField(FIELD.MsgType, MsgType.NewOrderSingle)
|
|
174
|
+
.setField(FIELD.ClOrdID, 'order-1')
|
|
175
|
+
.setField(FIELD.Symbol, 'AAPL');
|
|
176
|
+
|
|
177
|
+
const settings = SessionSettings.fromString(`
|
|
178
|
+
[DEFAULT]
|
|
179
|
+
ConnectionType=initiator
|
|
180
|
+
SocketConnectHost=127.0.0.1
|
|
181
|
+
SocketConnectPort=5001
|
|
182
|
+
HeartBtInt=30
|
|
183
|
+
UseDataDictionary=N
|
|
184
|
+
StartTime=00:00:00
|
|
185
|
+
EndTime=00:00:00
|
|
186
|
+
|
|
187
|
+
[SESSION]
|
|
188
|
+
BeginString=FIX.4.4
|
|
189
|
+
SenderCompID=CLIENT
|
|
190
|
+
TargetCompID=BROKER
|
|
191
|
+
`);
|
|
192
|
+
|
|
193
|
+
const initiator = new Initiator({
|
|
194
|
+
settings,
|
|
195
|
+
store: 'memory',
|
|
196
|
+
log: 'none',
|
|
197
|
+
handlers: {
|
|
198
|
+
onLogon: (id) => console.log('logged on:', id.toString()),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
initiator.start();
|
|
203
|
+
// initiator.stop() when done.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## API overview
|
|
207
|
+
|
|
208
|
+
The full type surface is documented in the shipped TypeScript declarations
|
|
209
|
+
(`dist/esm/index.d.ts`); the reference below is a concise summary. All exports
|
|
210
|
+
come from `@homaiohq/napi-quickfix`.
|
|
211
|
+
|
|
212
|
+
### Message
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
new Message(raw?: string, opts?: MessageParseOptions);
|
|
216
|
+
Message.parse(raw: string, opts?: MessageParseOptions): Message;
|
|
217
|
+
// MessageParseOptions: { validate?: boolean; dictionary?: DataDictionary;
|
|
218
|
+
// sessionDictionary?: DataDictionary; applicationDictionary?: DataDictionary }
|
|
219
|
+
|
|
220
|
+
message.setField(tag: number, value: string | number): this; // chainable, auto-routes header/trailer fields
|
|
221
|
+
message.getField(tag: number): string; // throws QuickFixError (FieldNotFound) if absent
|
|
222
|
+
// Standard header/trailer tags route to their section. Any other tag is a body field, unless the
|
|
223
|
+
// header or trailer already holds it (a field a dictionary declares there): then it is read and
|
|
224
|
+
// edited in place. A new dictionary-declared header field goes in with setHeaderField.
|
|
225
|
+
message.getFieldIfSet(tag: number): string | undefined;
|
|
226
|
+
message.isSetField(tag: number): boolean;
|
|
227
|
+
message.removeField(tag: number): this;
|
|
228
|
+
message.setHeaderField(tag, value): this; message.getHeaderField(tag): string;
|
|
229
|
+
message.setTrailerField(tag, value): this; message.getTrailerField(tag): string;
|
|
230
|
+
message.fields(): [tag: number, value: string][]; // body fields in wire order (also iterable)
|
|
231
|
+
message.headerFields(); message.trailerFields(); // same shape
|
|
232
|
+
message.isEmpty(): boolean; message.totalFields(): number; message.clear(): this;
|
|
233
|
+
|
|
234
|
+
// Tags must be integers (positive for setters) and group indices integers: anything else throws a TypeError.
|
|
235
|
+
// Repeating groups (indices are 1-based, as in QuickFIX)
|
|
236
|
+
message.addGroup(group: Group): this; // appends a copy, maintains the count field; TypeError if group.delim differs from the stored entries'
|
|
237
|
+
message.getGroup(index: number, tag: number): Group; // snapshot copy; throws FieldNotFound if absent
|
|
238
|
+
message.replaceGroup(index: number, group: Group): this; // throws FieldNotFound if absent, TypeError if group.delim differs
|
|
239
|
+
message.removeGroup(tag: number): this; message.removeGroup(index: number, tag: number): this; // (index, tag) throws FieldNotFound if absent
|
|
240
|
+
message.hasGroup(tag: number): boolean; message.hasGroup(index: number, tag: number): boolean;
|
|
241
|
+
message.groupCount(tag: number): number;
|
|
242
|
+
|
|
243
|
+
message.getMsgType(): string;
|
|
244
|
+
message.toString(): string; // raw SOH-delimited wire string
|
|
245
|
+
message.toPretty(): string; // human-readable
|
|
246
|
+
message.toJSON(): { msgType?: string; raw: string };
|
|
247
|
+
|
|
248
|
+
createMessage(fields?: Record<number, string | number>): Message;
|
|
249
|
+
parseMessage(raw: string, opts?: MessageParseOptions): Message;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Field values are accepted as `string | number` (numbers are stringified) and
|
|
253
|
+
always returned as `string`. Header/trailer fields such as `MsgType`,
|
|
254
|
+
`BeginString`, and `CheckSum` are **automatically routed** to the correct
|
|
255
|
+
section by `setField`, so you rarely need `setHeaderField`/`setTrailerField`
|
|
256
|
+
directly.
|
|
257
|
+
|
|
258
|
+
**Parsing and repeating groups.** A parse without a dictionary is *flat*: the
|
|
259
|
+
fields of a repeating group are kept as plain repeated body fields and
|
|
260
|
+
`getGroup` throws. Pass a `dictionary` (or a `sessionDictionary` /
|
|
261
|
+
`applicationDictionary` pair for FIXT/FIX 5) to parse groups structurally.
|
|
262
|
+
With `UseDataDictionary=Y` (the default), messages delivered to engine handlers
|
|
263
|
+
are already parsed with the session's dictionary, so `getGroup` works on them
|
|
264
|
+
directly; with `UseDataDictionary=N` they arrive flat.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { DataDictionary, Group, Message, FIELD, MsgType } from '@homaiohq/napi-quickfix';
|
|
268
|
+
|
|
269
|
+
const dd = DataDictionary.fromFile('./spec/FIX44.xml');
|
|
270
|
+
|
|
271
|
+
const order = new Message()
|
|
272
|
+
.setField(FIELD.MsgType, MsgType.NewOrderSingle)
|
|
273
|
+
.setField(FIELD.ClOrdID, 'order-1')
|
|
274
|
+
.addGroup(new Group(FIELD.NoPartyIDs, FIELD.PartyID).setField(FIELD.PartyID, 'TRADER-1').setField(FIELD.PartyRole, 11))
|
|
275
|
+
.addGroup(new Group(FIELD.NoPartyIDs, FIELD.PartyID).setField(FIELD.PartyID, 'FIRM-7').setField(FIELD.PartyRole, 1));
|
|
276
|
+
// 453=2|448=TRADER-1|452=11|448=FIRM-7|452=1 on the wire
|
|
277
|
+
|
|
278
|
+
const parsed = Message.parse(order.toString(), { dictionary: dd });
|
|
279
|
+
parsed.groupCount(FIELD.NoPartyIDs); // 2
|
|
280
|
+
parsed.getGroup(2, FIELD.NoPartyIDs).getField(FIELD.PartyID); // 'FIRM-7'
|
|
281
|
+
|
|
282
|
+
// getGroup returns a snapshot: write changes back with replaceGroup.
|
|
283
|
+
const first = parsed.getGroup(1, FIELD.NoPartyIDs).setField(FIELD.PartyRole, 12);
|
|
284
|
+
parsed.replaceGroup(1, first);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Group
|
|
288
|
+
|
|
289
|
+
One entry of a repeating group, identified by its **count tag** (`field`, e.g.
|
|
290
|
+
`453 NoPartyIDs`) and its **delimiter** (`delim`, the first tag of every entry,
|
|
291
|
+
e.g. `448 PartyID`). Groups nest through the same `addGroup`/`getGroup` API.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
new Group(field: number, delim: number, order?: number[]); // order: full entry field order, delimiter first, no tag twice
|
|
295
|
+
// delim and order tags are capped at 2^20 (RangeError beyond that)
|
|
296
|
+
group.field; group.delim; // read-only
|
|
297
|
+
|
|
298
|
+
group.setField(tag, value: string | number): this; group.getField(tag): string;
|
|
299
|
+
group.getFieldIfSet(tag): string | undefined; group.isSetField(tag): boolean;
|
|
300
|
+
group.removeField(tag): this; group.isEmpty(); group.totalFields(); group.clear(): this;
|
|
301
|
+
group.fields(): [tag, value][]; // in wire order (also iterable)
|
|
302
|
+
group.toString(): string; // the entry's fields, SOH-delimited, in wire order
|
|
303
|
+
|
|
304
|
+
// Nested groups — same contract as on Message
|
|
305
|
+
group.addGroup(g: Group): this; group.getGroup(index, tag): Group; group.replaceGroup(index, g): this;
|
|
306
|
+
group.removeGroup(tag): this; group.removeGroup(index, tag): this;
|
|
307
|
+
group.hasGroup(tag): boolean; group.hasGroup(index, tag): boolean; group.groupCount(tag): number;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Without an explicit `order`, an entry's fields are emitted delimiter-first and
|
|
311
|
+
then by ascending tag number, which matches the standard dictionaries for most
|
|
312
|
+
groups; pass `order` (e.g. `[448, 447, 452]`) when a group's spec order differs.
|
|
313
|
+
|
|
314
|
+
### SessionID
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
new SessionID(beginString: string, senderCompID: string, targetCompID: string, qualifier?: string);
|
|
318
|
+
sessionID.beginString; sessionID.senderCompID;
|
|
319
|
+
sessionID.targetCompID; sessionID.sessionQualifier;
|
|
320
|
+
sessionID.toString(); // e.g. "FIX.4.4:CLIENT->BROKER"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### SessionSettings
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
SessionSettings.fromString(cfg: string): SessionSettings;
|
|
327
|
+
SessionSettings.fromFile(path: string): SessionSettings;
|
|
328
|
+
settings.getSessions(): SessionID[];
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### DataDictionary
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
DataDictionary.fromString(xml: string): DataDictionary;
|
|
335
|
+
DataDictionary.fromFile(xmlPath: string): DataDictionary;
|
|
336
|
+
dictionary.validate(msg: Message, bodyOnly?: boolean): void; // throws QuickFixError if invalid
|
|
337
|
+
dictionary.getVersion(): string; // e.g. 'FIX.4.4'
|
|
338
|
+
dictionary.getFieldName(tag: number): string | undefined;
|
|
339
|
+
dictionary.getFieldTag(name: string): number | undefined;
|
|
340
|
+
dictionary.isField(tag: number): boolean;
|
|
341
|
+
dictionary.isMsgType(msgType: string): boolean;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`validate(msg, true)` validates as the *application* dictionary only: the
|
|
345
|
+
BeginString version check and header/trailer validation are skipped. A
|
|
346
|
+
dictionary also drives structural parsing — see `Message.parse` above.
|
|
347
|
+
|
|
348
|
+
### Initiator / Acceptor (the session engine)
|
|
349
|
+
|
|
350
|
+
Both extend Node's `EventEmitter` and share the same options and lifecycle:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
new Initiator({ settings, handlers?, store?: 'file' | 'memory', log?: 'screen' | 'file' | 'none' });
|
|
354
|
+
new Acceptor({ settings, handlers?, store?, log? });
|
|
355
|
+
|
|
356
|
+
engine.start(): Promise<void>;
|
|
357
|
+
engine.stop(force?: boolean): Promise<void>; // graceful drain by default; force to abort
|
|
358
|
+
engine.isLoggedOn(sessionID?: SessionID): boolean; // any session, or just that one
|
|
359
|
+
engine.getSessions(): SessionID[]; // the [SESSION]s it was configured with
|
|
360
|
+
engine.getSession(sessionID: SessionID): Session | undefined;
|
|
361
|
+
engine.ref(): void; // keep the event loop alive while running (default)
|
|
362
|
+
engine.unref(): void; // let a short script / test exit
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`start`/`stop` resolve once the engine has finished starting up / shutting down;
|
|
366
|
+
they run off the main thread so your handlers can fire meanwhile. Once `stop()`
|
|
367
|
+
settles the engine's sessions are destroyed: `getSession` returns `undefined` and
|
|
368
|
+
any `Session` handle you still hold throws `SessionNotFound`. Until then they are
|
|
369
|
+
still live: a graceful `stop()` settles only after the `'logout'` events it
|
|
370
|
+
triggered have been delivered, so a `'logout'` listener can still read the
|
|
371
|
+
session, and a `sendToTarget()` that has already reached its session completes
|
|
372
|
+
before the sessions go away. One that has not reached it by then is refused
|
|
373
|
+
with `SessionNotFound` rather than run against the engine that is gone; a send
|
|
374
|
+
is bound to the engine that owns the session when it is called, never to one
|
|
375
|
+
created later with the same id.
|
|
376
|
+
`stop(true)` discards the events still queued and destroys the sessions right
|
|
377
|
+
away. A `stop()` called while another is still in flight (from that `'logout'`
|
|
378
|
+
listener, say) returns the same Promise, so it too settles only once the
|
|
379
|
+
sessions are gone; the first call's `force` applies to both.
|
|
380
|
+
|
|
381
|
+
Two live engines cannot share a `SessionID`: constructing one whose session
|
|
382
|
+
already exists in the process throws a `ConfigError` ("Duplicate Session") until
|
|
383
|
+
the engine owning it has been stopped. A constructor that fails on a later
|
|
384
|
+
`[SESSION]` (any `ConfigError`) cleans up the sessions it had already created
|
|
385
|
+
(so the ids are not locked out by the failed attempt) and the engine-level log
|
|
386
|
+
QuickFIX had already opened for it.
|
|
387
|
+
|
|
388
|
+
### Session
|
|
389
|
+
|
|
390
|
+
A handle on one **live** session, obtained from `engine.getSession(id)` or the
|
|
391
|
+
module-level `lookupSession(id)`. It stores only the `SessionID` and the identity
|
|
392
|
+
of the engine it came from, and re-resolves the engine-owned `FIX::Session` on
|
|
393
|
+
every call, so it never dangles — once the session is gone (its engine was
|
|
394
|
+
stopped) every method throws a `QuickFixError` whose `fixErrorName` is
|
|
395
|
+
`'SessionNotFound'`, even after a new engine has been created with the same id.
|
|
396
|
+
A handle never crosses over to another engine's session; get a new one from the
|
|
397
|
+
new engine.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
session.sessionID: SessionID;
|
|
401
|
+
|
|
402
|
+
// State (sync)
|
|
403
|
+
session.isLoggedOn(): boolean; session.isEnabled(): boolean;
|
|
404
|
+
session.sentLogon(): boolean; session.sentLogout(): boolean; session.receivedLogon(): boolean;
|
|
405
|
+
session.isInitiator(): boolean; session.isAcceptor(): boolean;
|
|
406
|
+
session.isSessionTime(now?: Date): boolean; session.isLogonTime(now?: Date): boolean;
|
|
407
|
+
|
|
408
|
+
// Sequence numbers (sync; setters write to the message store -- on a quiet session, see below)
|
|
409
|
+
session.getExpectedSenderNum(): number; session.getExpectedTargetNum(): number;
|
|
410
|
+
session.setNextSenderMsgSeqNum(n: number): void; session.setNextTargetMsgSeqNum(n: number): void;
|
|
411
|
+
|
|
412
|
+
// Runtime options (sync; change the live session, not its SessionSettings)
|
|
413
|
+
session.getResetOnLogon() / setResetOnLogon(v: boolean) // likewise: ResetOnLogout,
|
|
414
|
+
session.getLogonTimeout() / setLogonTimeout(seconds: number) // ResetOnDisconnect, RefreshOnLogon,
|
|
415
|
+
session.getTimestampPrecision() / setTimestampPrecision(0..9) // CheckCompId, CheckLatency, MaxLatency,
|
|
416
|
+
// LogoutTimeout, PersistMessages,
|
|
417
|
+
// SendRedundantResendRequests,
|
|
418
|
+
// ValidateLengthAndChecksum,
|
|
419
|
+
// SendNextExpectedMsgSeqNum, IsNonStopSession
|
|
420
|
+
|
|
421
|
+
// Control
|
|
422
|
+
session.logon(): void; // enable; an initiator reconnects + logs on
|
|
423
|
+
session.logout(reason?: string): void; // disable + graceful Logout; stays down until logon()
|
|
424
|
+
session.refresh(): void; // re-read state from the message store
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Everything is synchronous: none of these members takes the mutex QuickFIX holds
|
|
428
|
+
while it runs your handlers, so they cannot deadlock the main thread — and
|
|
429
|
+
because they run inline, `session.logout('x'); session.logon();` takes effect in
|
|
430
|
+
that order. (`logon()`/`logout()` flip a plain flag the engine's network thread
|
|
431
|
+
reads without a lock, exactly as QuickFIX's own `Initiator::stop()` does from
|
|
432
|
+
the stopping thread; the engine picks the change up on its next pass.) The
|
|
433
|
+
sequence-number setters are likewise not serialised against a message the
|
|
434
|
+
engine is sending or receiving on that session at the same moment: QuickFIX
|
|
435
|
+
reads the number, runs your handler, then increments from the store under a
|
|
436
|
+
lock the setters do not take, so a value set in that window — from a handler,
|
|
437
|
+
or while a `sendToTarget()` or a heartbeat is in flight — is overwritten and the
|
|
438
|
+
session ends up one off from its counterparty. Renumber a **quiet** session:
|
|
439
|
+
before `start()`, from a `'logout'` event, or when you know nothing is in
|
|
440
|
+
flight. QuickFIX's `Session::disconnect()` and `reset()` are deliberately **not**
|
|
441
|
+
exposed: they drop the transport through the engine's socket monitor, which is
|
|
442
|
+
only safe from the engine's own network thread (QuickFIX itself never calls them
|
|
443
|
+
from anywhere else). Bring a session down with `logout()` and renumber it with
|
|
444
|
+
the sequence-number setters. Observe the outcome through the engine's
|
|
445
|
+
`'logon'` / `'logout'` events:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
initiator.on('logon', (id) => {
|
|
449
|
+
console.log('next outbound seq', initiator.getSession(id)!.getExpectedSenderNum());
|
|
450
|
+
});
|
|
451
|
+
initiator.on('logout', (id) => {
|
|
452
|
+
// The session is quiet now: renumber it, e.g. after the counterparty reset.
|
|
453
|
+
initiator.getSession(id)?.setNextTargetMsgSeqNum(1);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
lookupSession(id)?.logout('maintenance'); // -> 'logout' event, isLoggedOn() === false
|
|
457
|
+
lookupSession(id)?.logon(); // -> reconnects, 'logon' event
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Handlers vs. events
|
|
461
|
+
|
|
462
|
+
This is the key distinction to understand:
|
|
463
|
+
|
|
464
|
+
- **`handlers`** (passed in the constructor options) run **synchronously** on the
|
|
465
|
+
engine's callback path. They are the *only* place you can affect a message:
|
|
466
|
+
- `toAdmin` / `toApp` may **mutate** the outbound message in place (e.g. add
|
|
467
|
+
credentials to a Logon in `toAdmin`).
|
|
468
|
+
- `toApp` may **throw** `new FixReject('DoNotSend')` to suppress sending.
|
|
469
|
+
- `fromAdmin` / `fromApp` may **throw** a `FixReject` to reject an inbound
|
|
470
|
+
message (e.g. `'RejectLogon'`, `'UnsupportedMessageType'`).
|
|
471
|
+
- `onCreate` / `onLogon` / `onLogout` are fire-and-forget notifications.
|
|
472
|
+
|
|
473
|
+
- **Events** (`engine.on('logon', ...)`, etc.) are **observe-only**. The engine
|
|
474
|
+
emits `'create' | 'logon' | 'logout' | 'toAdmin' | 'fromAdmin' | 'toApp' | 'fromApp'`
|
|
475
|
+
*after* the corresponding handler runs. Event listeners **cannot** mutate a
|
|
476
|
+
message or reject it — use `handlers` for that.
|
|
477
|
+
|
|
478
|
+
### Rejections
|
|
479
|
+
|
|
480
|
+
Throw a `FixReject` from a handler to reject:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { FixReject, fixReject } from '@homaiohq/napi-quickfix';
|
|
484
|
+
|
|
485
|
+
throw new FixReject('DoNotSend');
|
|
486
|
+
throw new FixReject('RejectLogon', 'bad credentials');
|
|
487
|
+
throw fixReject('UnsupportedMessageType'); // convenience factory
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Recognized kinds: `'DoNotSend'`, `'RejectLogon'`, `'UnsupportedMessageType'`,
|
|
491
|
+
`'IncorrectDataFormat'`, `'IncorrectTagValue'`, `'FieldNotFound'`.
|
|
492
|
+
|
|
493
|
+
### Module functions and constants
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
sendToTarget(message: Message, sessionID: SessionID): Promise<boolean>;
|
|
497
|
+
sendToTarget(message: Message, qualifier?: string): Promise<boolean>; // session from the message header (8/49/56)
|
|
498
|
+
|
|
499
|
+
lookupSession(sessionID: SessionID): Session | undefined; // any engine in this process
|
|
500
|
+
doesSessionExist(sessionID: SessionID): boolean;
|
|
501
|
+
getSessions(): SessionID[]; // every session in this process, across all engines
|
|
502
|
+
numSessions(): number;
|
|
503
|
+
|
|
504
|
+
version(): string; // engine / addon version string
|
|
505
|
+
|
|
506
|
+
FIELD // field-name -> tag number, e.g. FIELD.MsgType === 35, FIELD.Password === 554
|
|
507
|
+
MsgType // MsgType values, e.g. MsgType.Logon === 'A'
|
|
508
|
+
Side // Side values, e.g. Side.Buy === '1'
|
|
509
|
+
OrdType // e.g. OrdType.Limit === '2'
|
|
510
|
+
TimeInForce // e.g. TimeInForce.Day === '0'
|
|
511
|
+
VALUES // every value group keyed by field name, e.g. VALUES.ExecType.Fill === '2'
|
|
512
|
+
enums // the full frozen tree: every value group plus FIELD
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`FIELD` is generated from the bundled QuickFIX's `FixFieldNumbers.h`, so it has every
|
|
516
|
+
`FIX::FIELD::*` name the engine knows (6000+). Values are literal types: `FIELD.Password`
|
|
517
|
+
is typed `554`, and a misspelt field name is a compile error. `FieldName` is the union of
|
|
518
|
+
all field names.
|
|
519
|
+
|
|
520
|
+
The value groups are generated the same way from `FixValues.h`: one frozen object per
|
|
521
|
+
field (690 groups, 5700+ values), each value a string because FIX is string-on-the-wire
|
|
522
|
+
(`EncryptMethod.None === '0'`, not `0`). Names derive from QuickFIX's
|
|
523
|
+
`FIX::<Field>_<VALUE>` constants with the SCREAMING_SNAKE suffix turned into PascalCase —
|
|
524
|
+
`Side_SELL_SHORT` → `Side.SellShort`, `ExecType_DONE_FOR_DAY` → `ExecType.DoneForDay` —
|
|
525
|
+
while `MsgType`, whose upstream names already mirror message names, is kept verbatim
|
|
526
|
+
(`MsgType.NewOrderSingle`, `MsgType.IOI`). Every word is lower-cased, acronyms included
|
|
527
|
+
(`SecurityIDSource_ISIN_NUMBER` → `SecurityIDSource.IsinNumber`, `MDEntryType_VWAP` →
|
|
528
|
+
`MDEntryType.Vwap`): the upstream names carry no acronym information, so there is no
|
|
529
|
+
allowlist to maintain and the keys are stable. The exact rule is documented in
|
|
530
|
+
`scripts/gen-values.mjs`. Values are literal types too (`Side.Buy` is typed `'1'`), and
|
|
531
|
+
`ValueGroupName` is the union of all group names.
|
|
532
|
+
|
|
533
|
+
The root entry exports `MsgType`, `Side`, `OrdType` and `TimeInForce` by name and every
|
|
534
|
+
group through `VALUES` / `enums`. Both tables are also exposed as subpath exports that
|
|
535
|
+
do **not** load the native addon, for tooling that only needs the constants (log
|
|
536
|
+
parsers, test helpers, bundles for platforms without a prebuild); each group is a named
|
|
537
|
+
export there, so bundlers resolving the ESM build keep only the groups you import (the
|
|
538
|
+
CJS build is not tree-shakeable: tsc drops the `@__PURE__` annotations when it emits
|
|
539
|
+
CommonJS):
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { FIELD } from '@homaiohq/napi-quickfix/fields';
|
|
543
|
+
import { ExecType, OrdStatus, SecurityType } from '@homaiohq/napi-quickfix/values';
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Errors
|
|
547
|
+
|
|
548
|
+
Errors thrown by native calls are `Error`s with `name === 'QuickFixError'`,
|
|
549
|
+
carrying a `fixError` discriminator (a stable string such as `'ConfigError'`,
|
|
550
|
+
`'FieldNotFound'`, `'InvalidMessage'`, `'IncorrectTagValue'`) plus a `detail`
|
|
551
|
+
string, so you can branch on the failure kind:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
try {
|
|
555
|
+
dictionary.validate(msg);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
if (err.name === 'QuickFixError' && err.fixError === 'FieldNotFound') {
|
|
558
|
+
// handle a missing required field
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
## Building from source / Contributing
|
|
564
|
+
|
|
565
|
+
Development uses **Yarn (Berry 4)** with the `node-modules` linker, via corepack.
|
|
566
|
+
|
|
567
|
+
```sh
|
|
568
|
+
corepack enable
|
|
569
|
+
yarn install
|
|
570
|
+
yarn build # cmake-js native build + dual (ESM + CJS) TypeScript build
|
|
571
|
+
yarn test # node --test
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
QuickFIX is fetched at build time via CMake `FetchContent` (pinned to
|
|
575
|
+
`v1.16.0`) — nothing is vendored or committed. The native build therefore needs
|
|
576
|
+
CMake (>= 3.12) and a C++17 compiler; the QuickFIX sources are downloaded
|
|
577
|
+
automatically on first configure.
|
|
578
|
+
|
|
579
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more detail, and
|
|
580
|
+
[ARCHITECTURE.md](./ARCHITECTURE.md) for a diagrammed walkthrough of how the
|
|
581
|
+
TypeScript API, the C++ Node-API addon, and the QuickFIX engine fit together
|
|
582
|
+
(layers, build pipeline, the threading/async model, and CI).
|
|
583
|
+
|
|
584
|
+
## Compatibility
|
|
585
|
+
|
|
586
|
+
| | |
|
|
587
|
+
| --- | --- |
|
|
588
|
+
| **Node.js** | LTS **22**, **24** and **26** (current) |
|
|
589
|
+
| **N-API version** | 9 |
|
|
590
|
+
| **Platforms** | Linux (glibc + musl/Alpine), macOS, Windows |
|
|
591
|
+
| **Prebuilt targets** | `linux-x64` (glibc, musl), `darwin-x64`, `darwin-arm64`, `win32-x64` |
|
|
592
|
+
| **libc (Linux)** | glibc **and** musl — separate libc-tagged prebuilds; musl is x64-only |
|
|
593
|
+
| **QuickFIX** | v1.16.0 (statically linked into prebuilds) |
|
|
594
|
+
| **Timestamp resolution** | microseconds on Linux (glibc + musl) and macOS; **milliseconds on Windows** |
|
|
595
|
+
|
|
596
|
+
Because prebuilds are N-API-tagged (not ABI-tagged), a single binary per
|
|
597
|
+
platform serves all supported Node LTS versions.
|
|
598
|
+
|
|
599
|
+
On Windows, QuickFIX has no microsecond clock available (it falls back to
|
|
600
|
+
`ftime()`), so sub-second `SendingTime` digits beyond milliseconds are always
|
|
601
|
+
zero — a `TimestampPrecision=6` session emits `.NNN000`. Setting a higher
|
|
602
|
+
precision is still valid and interoperable; only the extra digits are padding.
|
|
603
|
+
|
|
604
|
+
## Versioning
|
|
605
|
+
|
|
606
|
+
This package follows [Semantic Versioning](https://semver.org/) and is currently
|
|
607
|
+
in the **`0.x`** (pre-1.0) phase. While in `0.x`, **minor releases may include
|
|
608
|
+
breaking changes** — pre-1.0 has no separate major channel — so review the
|
|
609
|
+
[CHANGELOG](./CHANGELOG.md) before upgrading across a minor bump. Note that npm's
|
|
610
|
+
caret range locks the minor for `0.x`: `^0.1.0` resolves to `>=0.1.0 <0.2.0`.
|
|
611
|
+
|
|
612
|
+
The package version tracks *this binding's* API and behavior, not the bundled
|
|
613
|
+
QuickFIX release (statically linked, currently `v1.16.0` — see the table above).
|
|
614
|
+
See [VERSIONING.md](./VERSIONING.md) for the full policy.
|
|
615
|
+
|
|
616
|
+
## License
|
|
617
|
+
|
|
618
|
+
This package (`@homaiohq/napi-quickfix`) is licensed under the **MIT License** —
|
|
619
|
+
see [LICENSE](./LICENSE). Copyright (c) 2026 Homaio.
|
|
620
|
+
|
|
621
|
+
### Third-party licenses
|
|
622
|
+
|
|
623
|
+
The published prebuilt binaries **statically link** the QuickFIX C++ engine,
|
|
624
|
+
which is distributed under the **QuickFIX Software License, Version 1.0** (a
|
|
625
|
+
permissive, BSD-style license). Its full text and the required attribution are
|
|
626
|
+
reproduced in [THIRD_PARTY_LICENSES](./THIRD_PARTY_LICENSES).
|
|
627
|
+
|
|
628
|
+
As required by that license:
|
|
629
|
+
|
|
630
|
+
> This product includes software developed by quickfixengine.org
|
|
631
|
+
> (http://www.quickfixengine.org/).
|
|
632
|
+
|
|
633
|
+
### Acknowledgements / Naming
|
|
634
|
+
|
|
635
|
+
This is an **independent, unaffiliated** Node.js binding. It is not produced,
|
|
636
|
+
sponsored, or endorsed by quickfixengine.org or the QuickFIX project. The name
|
|
637
|
+
"QuickFIX" is used here **nominatively** — solely to describe the C++ library
|
|
638
|
+
that this package wraps — and does not imply any affiliation or endorsement. All
|
|
639
|
+
QuickFIX trademarks and names remain the property of their respective owners.
|